面试基础知识准备

网络

HTTP

参考链接

HTTP与HTTPS介绍
  • 超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息,HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息,因此,HTTP协议不适合传输一些敏感信息,比如:信用卡号、密码等支付信息。
  • 为了解决HTTP协议的这一缺陷,需要使用另一种协议:安全套接字层超文本传输协议HTTPS,为了数据传输的安全,HTTPS在HTTP的基础上加入了SSL/TLS协议,SSL/TLS依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。
  • HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。
  • HTTPS协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。
HTTP的头部
  • Accept:请求中可以接受的内容类型。
  • Connection
  • Content-Type:请求或响应中实际发送的内容类型。
  • Authorization:用于身份验证的凭证,例如基本认证。
  • User-Agent:发送请求的用户代理,例如浏览器类型和版本。
  • Referer:表示请求中的来源页面 URL。
  • Cache-Control:控制缓存的行为,例如缓存时间。
  • Host:指定请求的服务器名称。
  • Cookie:在客户端和服务器之间传递的 Cookie 数据。

在这里插入图片描述

HTTP的方法
  • GET : 用于请求指定资源的表示形式。请求参数包含在URL中,GET方法是幂等的。
  • POST :用于向指定资源提交数据,并请求服务器进行处理。请求参数包含在RequestBody中。非幂等。
  • PUT :用于向服务器上传新数据,更新。请求参数包含在RequestBody中(请求正文中)。幂等性。
  • DELETE :对这个资源的删操作。
  • HEAD :与GET方法的行为很类似,但服务器在响应中只返回实体的主体部分,对资源的首部进行检查。
  • TRACE :会在目的服务器端发起一个“回环”诊断。
  • OPTIONS方法:用于获取当前URL所支持的方法。若请求成功,则它会在HTTP头中包含一个名为“Allow”的头,值是所支持的方法,如“GET, POST”。在项目中的一个请求中header添加了一个token,使这个请求变成 了复杂请求,触发了预检请求。后来通过去掉头部的自定义token,让其变为简单请求。
HTTPS和HTTP的主要区别
  1. https协议需要到CA申请证书,一般免费证书较少,因而需要一定费用。
  2. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl/tls加密传输协议。
  3. http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
  4. http的连接很简单,是无状态的;HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
HTTP1-3的区别
HTTPS的主干层次
  1. 客户端生成一个随机的对称密钥。(加密通信就是双方都持有一个对称加密的秘钥,然后就可以安全通信了)
  2. 为了防止中间人篡改公钥,引入CA证书确保公钥是对应服务端生成的。
  3. 服务端把自己的公钥给 CA,CA用证书信息和公钥生成一个hash值,让 CA 用 CA 的私钥对hash值进行加密形成数字签名。(如果篡改了服务器的公钥,CA返回的数字签名经过客户端的CA公钥解密后的hash值与证书信息的hash值不匹配)。

在这里插入图片描述

CA证书的申请及其使用过程

非对称加密的缺点:

  1. 非对称加密时需要使用到接收方的公钥对消息进行加密,但是公钥不是保密的,任何人都可以拿到,中间人也可以。那么中间人可以做两件事,第一件是中间人可以在客户端与服务器交换公钥的时候,将客户端的公钥替换成自己的。这样服务器拿到的公钥将不是客户端的,而是中间人的。服务器也无法判断公钥来源的正确性。第二件是中间人可以不替换公钥,但是他可以截获客户端发来的消息,然后篡改,然后用服务器的公钥加密再发往服务器,服务器将收到错误的消息。
  2. 非对称加密的性能相对对称加密来说会慢上几倍甚至几百倍,比较消耗系统资源。正是因为如此,https将两种加密结合了起来。
    在这里插入图片描述

CA 签发证书的过程,如上图左边部分:

  1. ⾸先 CA 会把持有者的公钥、⽤途、颁发者、有效时间等信息打成⼀个包,然后对这些信息进⾏ Hash 计算, 得到⼀个 Hash 值;
  2. 然后 CA 会使⽤⾃⼰的私钥将该 Hash 值加密,⽣成 Certificate Signature,也就是 CA 对证书做了签名;
  3. 最后将 Certificate Signature 添加在⽂件证书上,形成数字证书;

客户端校验服务端的数字证书的过程,如上图右边部分:

  1. ⾸先客户端会使⽤同样的 Hash 算法获取该证书的 Hash 值 H1;
  2. 通常浏览器和操作系统中集成了 CA的公钥信息,浏览器收到证书后可以使⽤ CA 的公钥解密 Certificate Signature 内容,得到⼀个 Hash 值 H2;
  3. 最后⽐较 H1 和 H2,如果值相同,则为可信赖的证书,否则则认为证书不可信。

DNS解析过程

  • DNS 协议运行在 UDP 协议之上,使用端口号 53,用于将域名转换为IP地址
  • 浏览器先检查自身缓存中有没有被解析过这个域名对应的 ip 地址
  • 如果浏览器缓存没有命中,浏览器会检查操作系统缓存中有没有对应的已解析过的结果。在 windows 中可通过 c 盘里 hosts 文件来设置
  • 还没命中,请求本地域名服务器来解析这个域名,一般都会在本地域名服务器找到
  • 本地域名服务器没有命中,则去根域名服务器请求解析
  • 根域名服务器返回给本地域名服务器一个所查询域的主域名服务器
  • 本地域名服务器向主域名服务器发送请求
  • 接受请求的主域名服务器查找并返回这个域名对应的域名服务器的地址
  • 域名服务器根据映射关系找到 ip 地址,返回给本地域名服务器
  • 本地域名服务器缓存这个结果
  • 本地域名服务器将该结果返回给用户

JWT

JSON Web Token structure

在这里插入图片描述

xxxxx.yyyyy.zzzzz
  • Header

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    

    Then, this JSON is Base64Url encoded to form the first part of the JWT.

  • Payload

    {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true
    }
    

    The payload is then Base64Url encoded to form the second part of the JSON Web Token.

  • Signature

    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret)
    
JWT签名算法
  • HS256 (带有 SHA-256 的 HMAC )是一种对称加密算法, 双方之间仅共享一个密钥。由于使用相同的密钥生成签名和验证签名, 因此必须注意确保密钥不被泄密。

  • RS256 (采用SHA-256 的 RSA 签名) 是一种非对称加密算法, 它使用公共/私钥对: JWT的提供方采用私钥生成签名, JWT 的使用方获取公钥以验证签名。

TCP三次握手

在这里插入图片描述

  1. 第一步,Client会进入SYN_SENT状态,并发送Syn消息给Server端,SYN标志位在此场景下被设置为1,同时会带上Client这端分配好的Seq号,这个序列号是一个U32的整型数,该数值的分配是根据时间产生的一个随机值,通常情况下每间隔4ms会加1。除此之外还会带一个MSS,也就是最大报文段长度,表示Tcp传往另一端的最大数据块的长度。
  2. 第二步,Server端在收到,Syn消息之后,会进入SYN_RCVD状态,同时返回Ack消息给Client,用来通知Client,Server端已经收到SYN消息并通过了确认。这一步Server端包含两部分内容,一部分是回复Client的Syn消息,其中ACK=1,Seq号设置为Client的Syn消息的Seq数值+1;另一部分是主动发送Sever端的Syn消息给Client,Seq号码是Server端上面对应的序列号,当然Syn标志位也会设置成1,MSS表示的是Server这一端的最大数据块长度。
  3. 第三步,Client在收到第二步消息之后,首先会将Client端的状态从SYN_SENT变换成ESTABLISHED,此时Client发消息给Server端,这个方向的通道已经建立成功,Client可以发送消息给Server端了,Server端也可以成功收到这些消息。其次,Client端需要回复ACK消息给Server端,消息包含ACK状态被设置为1,Seq号码被设置成Server端的序列号+1。(备注:这一步往往会与Client主动发起的数据消息,合并到一起发送给Server端。)
  4. 第四步,Server端在收到这个Ack消息之后,会进入ESTABLISHED状态,到此时刻Server发向Client的通道连接建立成功,Server可以发送数据给Client,TCP的全双工连接建立完成。
  • 客户端向服务端发送同步信号,此时客户端进入同步发送状态。
  • 服务端接收到客户端的同步信号,对其进行确认,同时向客户端发送同步信号,进入同步接收状态。
  • 客户端接收到服务端的同步信号,对其确认,双方连接建立完成。
一台服务器​最大并发 TCP 连接数

在tcp应用中,server事先在某个固定端口监听,client主动发起连接,经过三路握手后建立tcp连接。那么对单机,其最大并发tcp连接数是多少呢?

如何标识一个TCP连接

在确定最大连接数之前,先来看看系统如何标识一个tcp连接。系统用一个四元组来唯一标识一个TCP连接
(源IP,源端口,目的IP,目的端口)。

client最大tcp连接数:
client每次发起tcp连接请求时,除非绑定端口,通常会让系统选取一个空闲的本地端口(local port),该端口是独占的,不能和其他tcp连接共享。tcp端口的数据类型是unsigned short,因此本地端口个数最大只有65536,端口0有特殊含义,不能使用,这样可用端口最多只有65535,所以在全部作为client端的情况下,一个client最大tcp连接数为65535,这些连接可以连到不同的serverip。

server最大tcp连接数
server通常固定在某个本地端口上监听,等待client的连接请求。不考虑地址重用(unix的SO_REUSEADDR选项)的情况下,即使server端有多个ip,本地监听端口也是独占的,因此server端tcp连接4元组中只有remoteip(也就是clientip)和remote port(客户端port)是可变的,因此最大tcp连接为客户端ip数×客户端port数,对IPV4,不考虑ip地址分类等因素,最大tcp连接数约为2的32次方(ip数)×2的16次方(port数),也就是server端单机最大tcp连接数约为2的48次方

为什么要三次握手

官方解释:The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
为了防止老的重复连接初始化导致的混乱。

第三次握手是为了防止已经失效的连接请求报文段突然又传到服务端,因而产生错误。
三次握手最主要的目的就是 双方确认 自己与对方的发送和接收是正常的。

详细分析:假设客户端发出去的第一个连接请求由于某些原因在网络节点中滞留了导致延迟,直到连接释放的某个时间点才到达服务端,这是一个早已失效的报文,但是此时服务端仍然认为这是客户端的建立连接请求第一次握手,于是服务端回应了客户端,第二次握手。
如果只有两次握手,那么到这里,连接就建立了,但是此时客户端并没有任何数据要发送,而服务端还在等待接收数据,造成很大的资源浪费。所以需要第三次握手,只有客户端再次回应一下,就可以避免这种情况。三次握手在不可靠的信道上建立了可靠的连接。

四次挥手

在这里插入图片描述
在这里插入图片描述

第一次挥手:

  • 客户端向服务器发送一个 FIN 数据包(FIN = 1,seq = u)主动断开连接,报文中会指定一个序列号。
  • 告诉服务器:我要跟你断开连接了,不会再给你发数据了;
  • 客户端此时还是可以接收数据的,如果一直没有收到被动连接方的确认包,则可以重新发送这个包。 此时客户端处于 FIN_WAIT1 状态。

第二次挥手:

  • 服务器收到 FIN 数据包之后,向客户端发送确认包(ACK = 1,ack = u + 1),把客户端的序列号值 + 1 作为 ACK报文的序列号值,表明已经收到客户端的报文了
  • 这是服务器在告诉客户端:我知道你要断开了,但是我还有数据没有发送完,等发送完了所有的数据就进行第三次挥手 此时服务端处于CLOSE_WAIT 状态,客户端处于 FIN_WAIT2 状态

第三次挥手:

  • 服务器向客户端发送FIN 数据包(FIN=1,seq = w),且指定一个序列号,以及确认包(ACK = 1, ack = u + 1),用来停止向客户端发送数据 这个动作是告诉客户端:我的数据也发送完了,不再给你发数据了
  • 此时服务端处于LAST_ACK状态,客户端处于TIME_WAIT状态

第四次挥手:

  • 客户端收到 FIN数据包 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值
  • 此时客户端处于 TIME_WAIT 状态。
  • 需要过一了一定时间(2MSL)之后,客户端发送确认包(ACK = 1, ack = w + 1),此时客户端才会进入 CLOSED 状态,以确保发送方的ACK可以到达接收方,防止已失效连接请求报文段出现在此连接中。
  • 至此,完成四次挥手。

总结

  • 客户端向服务端发送结束数据包,客户端进入结束等待状态1。
  • 服务端接收到客户端的结束数据包,对其进行确认,继续发送数据,服务端进入关闭等待状态。
  • 客户端接收到服务端确认数据包,进入结束等待状态。
  • 服务端发送数据完成后,向客户端发送结束数据包,服务端进入最后确认状态。
  • 客户端接收到服务端的结束数据包后,对其确认,进入TIME_WAIT状态。
  • 经过两个报文最大生存时间后双方断开连接。
为什么是两个MSL呢?
  • MSL(Maximum Segment Lifetime)报文最大生存时间
  • 第一个MSL是保证最后一次挥手客户端响应服务端的ACK到达了服务端
  • 第二个MSL 是保证服务端没有重发新的报文给客户端,没有超时重传
  • 即客户端发过去了,服务端也没有重传请求过来,说明它已经收到了,那么就可以关闭了。

如果不等待2*MSL会造成什么后果呢?

  • 客户端返回ACK (第四次挥手)后直接关闭,可能因为网络延迟导致服务端收不到ACK,一段时间后,服务端认为客户端没有收到FIN请求(第三次挥手),进行超时重发,但是客户端已经关闭了,不会给响应。理论上来说服务器超时重发5次后,就会主动断开连接,这样数据既不会丢失也不会错乱,是可以的,但是这样不符合可靠连接。
  • 如果客户端直接关闭,然后向服务器建立新连接,如果新连接和老连接的端口是一样的。假设老连接还有一些数据,因为网络或者其他原因,一直滞留没有发送成功,新连接建立后,就直接发送到新连接里面去了,造成数据的紊乱,因此,我们需要等到2*MSL,让滞留在网络中的报文失效,再去建立新的连接。
为什么要四次挥手?

TCP是全双工模式,接收到FIN意味着将没有数据再发来,但是还是可以继续发送数据。
如果只有三次,客户端发送完数据请求断开连接,而服务端不一定也同样发送完数据,若同时回ACK和FIN给客户端,断开连接,可能造成数据的损坏;

我们是否可以在服务器端数据传送完成后,再返回FIN+ACK呢?中间就可以省略一次ACK了?
如果服务端还有很多数据需要传送,耗时长,客户端在发送释放报文后,一直没有收到反馈,那么他会认为服务端没有收到我的FIN,因此就会不停的重发FIN。

TCP 和 UDP 的区别?
TCPUDP
连接面向连接无连接
可靠性可靠不可靠
数据流方式字节流报文流
速度
开销首部20个字节8字节
  • 连接:TCP面向连接的传输层协议,即传输数据之前必须先建立好连接;UDP无连接。

  • 服务对象:TCP点对点的两点间服务,即一条TCP连接只能有两个端点;UDP支持一对一,一对多,多对一,多对多的交互通信。

  • 可靠性:TCP可靠交付:无差错,不丢失,不重复,按序到达;UDP尽最大努力交付,不保证可靠交付。

  • 拥塞控制/流量控制:有拥塞控制和流量控制保证数据传输的安全性;UDP没有拥塞控制,网络拥塞不会影响源主机的发送效率。

  • 报文长度:TCP动态报文长度,即TCP报文长度是根据接收方的窗口大小和当前网络拥塞情况决定的;UDP面向报文,不合并,不拆分,保留上面传下来报文的边界。

  • 首部开销:TCP首部开销大,首部20个字节;UDP首部开销小,8字节(源端口,目的端口,数据长度,校验和)。

  • 适用场景(由特性决定):数据完整性需让位于通信实时性,则应该选用TCP 协议(如文件传输、重要状态的更新等);反之,则使用 UDP 协议(如视频传输、实时通信等)。

TCP拥塞控制

TCP发送方首先使用慢开始算法,拥塞窗口从1指数增长的慢开始门限值,然后使用拥塞避免算法,线性增加拥塞窗口,当发生超时重传的时候,将慢开始门限值更新为当前拥塞窗口的一半,拥塞窗口更新为1,执行慢开始算法,达到新的慢开始门限值的时候进行拥塞避免算法线性增加拥塞窗口的大小,当收到三个重复的确认报文时,进行快重传快恢复 ,将慢开始门限的值更新为当前拥塞窗口的一半,拥塞窗口的值更新为慢开始门限的值,然后执行拥塞避免算法线性增加拥塞窗口的大小。
在这里插入图片描述

Session存储在哪里,和Cookie区别

Session是一种服务器端的机制,用于存储用户相关的数据。在客户端发起请求时,服务器会创建一个唯一的Session ID,并将该ID存储在一个Cookie中返回给客户端。客户端将该Cookie保存在本地,每次发起请求时都会将该Cookie发送给服务器。服务器使用该Session ID来查找对应的Session数据,并在服务器端保存该数据。Session数据可以存储在服务器的内存中、硬盘上或者数据库中,具体存储位置取决于开发人员的配置。

相比之下,Cookie是一种客户端机制,它允许Web服务器将数据存储在客户端的浏览器中。当客户端发起请求时,浏览器会将该Cookie自动包含在请求中发送给服务器。Cookie通常用于存储一些持久化的数据,比如用户的登录信息、购物车状态等。

总的来说,Session和Cookie都是用于存储数据的机制,但是Session是一种服务器端的机制,存储的数据只存在于服务器端,而Cookie是一种客户端机制,存储的数据存在于客户端浏览器中。

操作系统

进程与线程与协程

进程
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

线程
线程有时被称为轻量级进程( Lightweight Process, LWP),是操作系统执行的最小单位。一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。

协程
协程是一种比线程更加轻量级的一种函数。一个线程可以拥有多个协程。协程是非抢占式的,由协程主动交出控制权,go function把函数交给调度器运行,调度器会在合适的时机进行切换,协程不是被操作系统内核所管理的,而是完全由程序所控制的,即在用户态执行。 这样带来的好处是:性能有大幅度的提升,因为不会像线程切换那样消耗资源。

Goroutine可以理解为一种Go语言的协程(轻量级线程),是Go支持高并发的基础,属于用户态的线程,由Go runtime管理而不是操作系统。

对比

  • 协程既不是进程也不是线程,协程仅是一个特殊的函数。协程、进程和线程不是一个维度的。
  • 一个进程可以包含多个线程,一个线程可以包含多个协程。虽然一个线程内的多个协程可以切换但是这多个协程是串行执行的,某个时刻只能有一个线程在运行,没法利用CPU的多核能力。
  • 协程与进程一样,也存在上下文切换问题。
  • 进程的切换者是操作系统,切换时机是根据操作系统自己的切换策略来决定的,用户是无感的。进程的切换内容包括页全局目录、内核栈和硬件上下文,切换内容被保存在内存中。进程切换过程采用的是“从用户态到内核态再到用户态”的方式,切换效率低。
  • 线程的切换者是操作系统,切换时机是根据操作系统自己的切换策略来决定的,用户是无感的。线程的切换内容包括内核栈和硬件上下文。线程切换内容被保存在内核栈中。线程切换过程采用的是“从用户态到内核态再到用户态”的方式,切换效率中等。
  • 协程的切换者是用户(编程者或应用程序),切换时机是用户自己的程序来决定的。协程的切换内容是硬件上下文,切换内容被保存在用自己的变量(用户栈或堆)中。协程的切换过程只有用户态(即没有陷入内核态),因此切换效率高。

内存管理

具有快表的分页管理方式地址变换

在这里插入图片描述

请求分页地址变换

在这里插入图片描述
在这里插入图片描述
段页式管理
在这里插入图片描述

为什么使用mmap完成磁盘IO

使用 mmap(memory-mapped I/O)完成磁盘 I/O 操作可以提高文件读写的性能和效率。这是因为 mmap 机制利用了内存映射技术,将文件内容映射到进程的虚拟地址空间中,从而实现进程直接读写这段内存的文件数据,避免了数据在内核态和用户态之间的多次复制,减少了系统调用的开销,使用 mmap 还可以避免缓存操作,提高多进程访问同一文件时的并发效率。

Nginx

同步与异步,阻塞与非阻塞

同步阻塞: 客户端发送请求给服务端,此时服务端处理任务时间很久,则客户端则被服务端堵塞了,所以客户端会一直等待服务端的响应,此时客户端不能做事,服务端也不会接受其他客户端的请求。这种通信机制比较简单粗暴,但是效率不高。

同步非阻塞: 客户端发送请求给服务端,此时服务端处理任务时间很久,这个时候虽然客户端会一直等待响应,但是服务端可以处理其他的请求,过一会回来的。这种方式很高效,一个服务端可以处理很多请求,不会在因为任务没有处理完而堵着,所以这是非阻塞的。

异步阻塞: 客户端发送请求给服务端,此时服务端处理任务时间很久,但是客户端不会等待服务器响应,它可以做其他的任务,等服务器处理完毕后再把结果端,客户端得到回调后再处理服务端的响应。这种方式可以避免客户端一直处于等待的状态,优化了用户体验,其实就是类似于网页里发起的ajax异步请求。

异步非阻塞: 客户端发送请求给服务端,此时服务端处理任务时间很久,这个时候的任务虽然处理时间会很久,但是客户端可以做其他的任务,因为他是异步回调函数里处理响应;同时服务端是非阻塞的,所以服务端可以去处理其他的任务,如此,这个模式就显得非常的高效了。

什么是反向代理

  • 用户请求目标服务器,由代理服务器决定访问哪个ip

什么是正向代理

  • 客户端请求目标服务器之间的一个代理服务器。
  • 请求会先经过代理服务器,然后再转发请求到目标服务器,获得内容后最后响应给客户端。

分布式

集中式系统
集中式系统通过一个应用实现,这个应用部署在一个机器上对外服务。集中式系统复杂、难于维护、一个功能的故障可能会使服务瘫痪、扩展性差。

分布式系统
分布式系统的服务被拆分成多个应用,分别提供不同的功能,每个应用部署在不同的机器上对外提供服务。分布式系统处理能力更强、可靠性更高、也有很好的扩展性。

分布式与集群
分布式(distributed)是指在多台不同的服务器中部署不同的服务模块,通过远程调用协同工作,对外提供服务。

集群(cluster)是指在多台不同的服务器中部署相同应用或服务模块,构成一个集群,通过负载均衡设备对外提供服务。

分布式计算
分布式计算将该应用分解成许多小的部分,分配给多台计算机进行处理。这样可以节约整体计算时间,大大提高计算效率。

优点:

  • 可用性:故障范围小
  • 扩展性
  • 系统模块化
  • 更高的性能:吞吐量大,响应时间慢。

缺点

  • 数据一致性:分布式事务的解决
  • 系统复杂型,管理运维难度大。
分布式系统中的CAP原理
  • C - Consistent ,一致性。具体是指,操作成功以后,所有的节点,在同一时间,看到的数据都是完全一致的。所以,一致性,说的就是数据一致性。
  • A - Availability ,可用性。指服务一致可用,在规定的时间内完成响应。
  • P - Partition tolerance ,分区容错性。指分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供服务。

CAP原理指出,这3个指标不能同时满足,最多只能满足其中的两个。

如果我们满足C(一致性),也就是说客户端无论访问A还是访问B,得到的结果都是一样的,那么现在A和B的数据不一致,需要等到A和B的数据一致以后,也就是同步恢复以后,才可对外提供服务。这样我们虽然满足了C(一致性),却不能满足A(可用性)。

所以,我们的系统在满足P(分区容错性)的同时,只能在A(可用性)和C(一致性)当中选择一个不能CAP同时满足。我们的分布式系统只能是AP或者CP。

BASE

BASE是Basically Available(基本可用), Soft-state(软状态), Eventually consistent(最终一致)的缩写。

  • Basically Available,基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。

  • 软状态( Soft State)

    软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有两到三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。

  • 最终一致性( Eventual Consistency)

    BASE是Basically Available(基本可用), Soft-state(软状态), Eventually consistent(最终一致)的缩写。

DDOS(分布式服务拒绝)

DDoS(Distributed Denial of Service)攻击是一种恶意行为,旨在通过使目标系统过载,使其无法正常提供服务。

  • 带宽攻击:攻击者通过向目标系统发送大量的网络流量,耗尽目标系统的带宽资源,导致网络拥塞和服务中断。

  • SYN Flood攻击:攻击者发送大量的TCP连接请求(SYN包),但不完成握手过程,使得目标系统耗尽资源来处理未完成的连接请求,导致服务不可用。

  • ICMP Flood攻击:攻击者发送大量的ICMP Echo请求(Ping请求),使得目标系统过载,无法正常响应合法的请求。

  • DNS Amplification攻击:攻击者利用存在开放递归查询的DNS服务器,发送伪造的请求,使其响应的数据比请求的数据更大,从而放大攻击流量,压倒目标系统。

  • HTTP Flood攻击:攻击者发送大量的HTTP请求,模拟合法用户的行为,耗尽目标系统的处理能力或网络带宽。

解决方法:
网络过滤和防火墙配置

Mysql

索引底层实现原理
  • 通过B+Tree来实现:B+Tree通过扩大阶数来降低树的高度。
  • B+树的叶子节点包含了全部的关键字信息,叶子节点按照关键字从小到大排序构成有序链表。
  • B+Tree中,非叶子节点仅用于索引,不保存数据记录,记录存放在叶子节点中。
  • 等值查询时B+Tree和B-Tree的区别不大,由于B+Tree的中间节点只用于存放索引,同样空间大小可以存放更多的关键字,减少查找次数,从而减少磁盘IO次数。
  • B+Tree范围查询时可以通过叶子节点的有序链表快速返回,B-Tree需要一个一个的查询之后再组装。
    在这里插入图片描述

InnoDB存储方式(聚簇索引)

  • 主键索引:叶子节点存储主键和数据,主键索引之外的就是非聚簇索引,非聚簇索引又叫辅助索引或者二级索引。

    # 主键索引的的叶子节点存储的是**一行完整的数据**,
    # 所以只需搜索主键索引的 B+Tree 就可以轻松找到全部数据
    select * from user where id = 1;
    
  • 非主键索引:叶子节点存储索引和主键,查到对应的主键再根据主键去查询数据。

    # 非主键索引的叶子节点存储的是**主键值**,
    # 所以MySQL会先查询到 name 列的索引的 B+Tree,搜索得到对应的主键值
    # 然后再去搜索该主键值查询主键索引的 B+Tree 才可以找到对应的数据
    select * from user where name = 'Jack';
    

    避免回表:使用覆盖索引,所谓覆盖索引就是指索引中包含了查询中的所有字段,这种情况下就不需要再进行回表查询了。

MyISAM存储方式(非聚簇索引)

  • 索引和数据分开存储:叶子节点都是存储指向数据块的指针。

Hash索引和B+树区别
在这里插入图片描述

  • B+树可以进行范围查询,Hash索引不能。
  • B+树支持联合 索引的最左侧原则,Hash索引不支持。
  • B+树支持order by排序,hash索引不支持
  • Hash索引在等值查询比B+树效率高。
  • B+树使用like 进行模糊查询的时候,like后面(比如%开头)的话可以起到优化的作用,Hash索引根本无法进行模糊查询。

Innodb与MyISAM的区别

  • Innodb支持事务,MyISAM不支持事务
  • Innodb支持外键,MyISAM不支持外键
  • Innodb支持mvcc(多版本并发控制),MyISAM不支持
  • select count(*) from table时,MyISAM更快,因为它有一个变量保存了整个表的总行数,可以直接读取,- InnoDB就需要全表扫描。
  • Innodb不支持全文索引,而MyISAM支持全文索引(5.7以后Innodb也支持全文索引)
  • Innodb支持表、行级锁,而MyISAM只支持表级锁。
  • Innodb必须有主键,而MyISAM可以没有主键。
  • Innodb表需要更多的内存和存储,而MyISAM可被压缩,存储空间较小。
  • Innodb按主键大小有序插入,MyISAM记录插入顺序是,按记录插入顺序保存。
  • InnoDB 存储引擎提供了具有提交、回滚、崩溃恢复能力的事务安全,与 MyISAM 比 InnoDB 写的效率差一些,并且会占用更多的磁盘空间以保留数据和索引

InnoDB引擎的4大特性

  • 插入缓冲(insert buffer)
  • 二次写(double write)
  • 自适应哈希索引(ahi)
  • 预读(read ahead)
索引失效
  • 索引列不独立。独立是指:列不能是表达式的一部分,也不能是函数的参数
  • 使用了左模糊
  • 使用or查询的部分字段没有索引
  • 字符串条件未使用单引号
  • 不符合最左原则的查询
  • 索引字段建议添加Not NULL约束
  • 隐式转换导致索引失效(查询类型和表结构类型不同,int,varchar)

索引优缺点

  • 优点:
    • 唯一索引可以保证数据库中每一行的数据的唯一性
    • 索引可以加快数据查询速度,减少查询时间
  • 缺点:
    • 创建索引和维护索引要消耗时间
    • 索引需要占物理空间,除了数据表占用数据空间之外,每个索引还要占用一定的物理空间
    • 以表中的数据进行增、删、改的时候,索引要动态的维护。
索引调优

创建索引的原则

  • 长字段的调优:利用CRC32建立一个hash字段,通过这个hash字段建立索引。
  • 使用列的类型小的创建索引
  • 最左前缀匹配原则
  • 频繁作为查询条件的字段才去创建索引,经常 GROUP BY 和 ORDER BY 的列
  • 数据量不大的表不建议创建索引,包含很多重复数据的字段不适合创建索引,例如sex。
  • 频繁更新的字段不适合创建索引
  • 索引列不能参与计算,不能有函数操作
最左前缀原则

最左前缀匹配原则,是一个非常重要的原则,可以通过以下这几个特性来理解。

  • 对于联合索引,MySQL 会一直向右匹配直到遇到范围查询(> , < ,between,like)就停止匹配。比如 a = 3 and b = 4 and c > 5 and d = 6,如果建立的是(a,b,c,d)这种顺序的索引,那么 d 是用不到索引的,但是如果建立的是 (a,b,d,c)这种顺序的索引的话,那么就没问题,而且 a,b,d 的顺序可以随意调换。
  • = 和 in 可以乱序,比如 a = 3 and b = 4 and c = 5 建立 (a,b,c)索引可以任意顺序。
  • 如果建立的索引顺序是 (a,b)那么直接采用 where b = 5 这种查询条件是无法利用到索引的,这一条最能体现最左匹配的特性。
索引为什么要符合最左前缀原则

是因为mysql创建联合索引时,首先会对最左边字段排序,也就是第一个字段,然后再在保证第一个字段有序的情况下,再排序第二个字段,以此类推。
所以联合索引最左列是绝对有序的,其他字段无序。

为什么不建议字段为null

1、存储空间优化
存储空间:空串是不占用存储空间的,而 NULL 会占用存储空间。

2、查询效率优化
含有空值的列很难进行查询优化,而且对表索引时不会存储 NULL 值的,所以如果索引的字段可以为 NULL,索引的效率会下降很多。因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用 0、一个特殊的值或者一个空串代替空值。

3、联表查询问题
联表查询的时候,例如 LEFT JOIN table2,若没有记录,则查找出的 table2 字段都是 null。假如 table2 有些字段本身可以是 null,那么除非把 table2 中 not null 的字段查出来,否则就难以区分到底是没有关联记录还是其他情况。

4、count(Null column) 问题
如果有 Null column 存在的情况下,count(Null column)需要格外注意,null 值不会参与统计。

5、子查询 NOT IN
NOT IN 子查询在有 NULL 值的情况下返回永远为空结果。

创建的三种方式

  • 在执行CREATE TABLE时创建索引。

    CREATE TABLE `employee` (
    `id` INT ( 11 ) NOT NULL,
    `name` VARCHAR ( 255 ) DEFAULT NULL,
    `age` INT ( 11 ) DEFAULT NULL,
    `date` datetime DEFAULT NULL,
    `sex` INT ( 1 ) DEFAULT NULL,
    PRIMARY KEY ( `id` ),
    KEY `idx_name` ( `name` ) USING BTREE 
    ) ENGINE = INNODB DEFAULT CHARSET = utf8;
    
  • 使用ALTER TABLE命令添加索引。

    ALTER TABLE table_name ADD INDEX index_name ( COLUMN );
    
  • 使用CREATE INDEX命令创建。

    CREATE INDEX index_name ON table_name ( COLUMN );
    

查看慢查询日志

set global log_output = 'FILE,TABLE';
set global slow_query_log = 'ON';
set global long_query_time = 5;

set global log_queries_not_using_indexes = 'ON';

SELECT * FROM `mysql`.slow_log

show variables like '%slow_query_log_file%'

分析一条SQL语句

set global log_output = 'FILE,TABLE';
set global slow_query_log = 'ON';
set global long_query_time = 5;

set global log_queries_not_using_indexes = 'ON';

SELECT * FROM `mysql`.slow_log

show variables like '%slow_query_log_file%'

Explain分析SQL执行计划
根据Id从大到小的顺序依次执行计划,查看这些计划的连接类型是否发生了全表扫描,rows字段查看扫描行数大小,key字段查看实际是否使用了索引,extra字段是否发生了文件排序。

项目中遇到的慢查询:项目中有一条SQL是试题关联试题提交数量以及试题信息等的一条查询,大概有100w条数据,通过explain分析了SQL的执行计划后,发现有一个执行计划发生了全表扫描,没有走索引,然后在提交表建立了一个试题Id和评测结果的索引,从5s降到百毫秒级别。

create index voj_submissions_problem_id_submission_judge_result_index on voj_submissions (problem_id, submission_judge_result);

分库分表
  • 水平分库:以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。
  • 水平分表:以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。
  • 垂直分库:以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。
  • 垂直分表:以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。
MYSQL事务

事务,由一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。

事务的四大特性分别是(ACID):原子性、一致性、隔离性、持久性

  • 原子性:功能不可分割,一组 SQL 操作要么全部成功,要么全部失败。原子性关注的是成功的状态。
  • 一致性:是指数据处于一种语义上的有意义且正确的状态。一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态。事务的中间状态不可见。一致性关注的是数据可见性。
  • 隔离性:多个用户并发访问数据库时,一个用户的事务不能被其他用户的事务所干扰,多个并发事务要相互隔离。
  • 持久化:持久化到磁盘。

Innodb的事务实现原理

  • 原子性:是使用 undo log来实现的,如果事务执行过程中出错或者用户执行了rollback,系统通过undo log日志返回事务开始的状态。
  • 持久性:使用 redo log来实现,只要redo log日志持久化了,当系统崩溃,即可通过redo log把数据恢复。
  • 隔离性:通过锁以及MVCC,使事务相互隔离开。
  • 一致性:通过回滚、恢复,以及并发情况下的隔离性,从而实现一致性。

事务的隔离级别有哪些?

  • 读未提交(Read Uncommitted):一个事务读取到了其他事务还未提交的数据
  • 读已提交(Read Committed):一个事务读取数据,允许其他事务对该数据进行读写操作,再次读取时发生了改变
  • 可重复读(Repeatable Read):在一个事务中多次读取数据,不允许其他事务修改数据。
  • 串行化(Serializable):序列化是最高的事务隔离级别,同时代价也是最高的,性能很低,事务只能顺序执行。

MySQL默认的事务隔离级别为可重复读(Repeatable Read)。
在Oracle数据库中,只支持Serializeble(串行化)级别和Read committed(读已提交)这两种级别,其中默认的为Read committed级别。

读未提交:脏读,不可重复读,幻读
读已提交:不可重复读,幻读
可重复读:幻读
串行化:隔离级别最高

幻读,脏读,不可重复读

  • 事务A、B交替执行,事务A被事务B干扰到了,因为事务A读取到事务B未提交的数据,这就是脏读
  • 在一个事务范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。
  • 事务A查询一个范围的结果集,另一个并发事务B往这个范围中插入/删除了数据,并静悄悄地提交,然后事务A再次查询相同的范围,两次读取得到的结果集不一样了,这就是幻读。

MySql隔离级别的实现原理

实现隔离机制的方法主要有两种:

  • 读写锁
  • 一致性快照读,即 MVCC

MySql使用不同的锁策略(Locking Strategy)/MVCC来实现四种不同的隔离级别。RR、RC的实现原理跟MVCC有关,RU和Serializable跟锁有关。

MVCC的实现原理

MVCC,中文叫多版本并发控制,它是通过读取历史版本的数据,来降低并发事务冲突,从而提高并发性能的一种机制。它的实现依赖于隐式字段、undo日志、快照读&当前读、Read View。

每一个事务在进行更新操作的时候都会生成一条记录,每一条记录都会有记录事务Id,回滚指针,通过回滚指针连接形成版本链。当有读操作时,顺着版本链中最新的记录往下找,根据Readview(包含当前活跃(未提交)事务的数组和最大事务Id)对事务的可见性进行判断,判断当前事务可见哪些版本。

隐式字段

对于InnoDB存储引擎,每一行记录都有两个隐藏列DBTRXID,DBROLLPTR,如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列 DBROWID。

  • DBTRXID,记录每一行最近一次修改(修改/更新)它的事务ID,大小为6字节;
  • DBROLLPTR,这个隐藏列就相当于一个指针,指向回滚段的undo日志,大小为7字节;
  • DBROWID,单调递增的行ID,大小为6字节;
columncolumncolumnDBTRXIDDBROLLPTRDBROWID

undo日志

  • 事务未提交的时候,修改数据的镜像(修改前的旧版本),存到undo日志里。以便事务回滚时,恢复旧版本数据,撤销未提交事务数据对数据库的影响。
  • undo日志是逻辑日志。可以这样认为,当delete一条记录时,undo log中会记录一条对应的insert记录,当update一条记录时,它记录一条对应相反的update记录。
  • 存储undo日志的地方,就是回滚段。

多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(DBROLLPTR)连一条Undo日志链。

快照读&当前读

  • 快照读
    读取的是记录数据的可见版本(有旧的版本),不加锁,普通的select语句都是快照读,如:

    select * from account where id > 2;
    
  • 当前读
    读取的是记录数据的最新版本,显示加锁的都是当前读。

    select * from account where id > 2 lock in share mode;
    select * from account where id > 2 for update;
    

Read View

  • Read View就是事务执行快照读时,产生的读视图。

  • 事务执行快照读时,会生成数据库系统当前的一个快照,记录当前系统中还有哪些活跃的读写事务,把它们放到一个列表里。

  • Read View主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据~

  • m_ids:当前系统中那些活跃的读写事务ID,它数据结构为一个List。

  • minlimitid:m_ids事务列表中,最小的事务ID

  • maxlimitid:m_ids事务列表中,最大的事务ID

  • 如果DBTRXID < minlimitid,表明生成该版本的事务在生成ReadView前已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。

  • 如果DBTRXID > m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。

  • 如果 minlimitid =<dbTRXID<= maxlimitid,需要判断mids.contains(DBTRX_ID),如果在,则代表Read View生成时刻,这个事务还在活跃,还没有Commit,你修改的数据,当前事务也是看不见的;如果不在,则说明,你这个事务在Read View生成之前就已经Commit了,修改的结果,当前事务是能看见的。

    RC每次读取数据前都生成一个ReadView,而RR只在第一次读取数据时生成一个ReadView。

事务一事务二事务三
update table set name = 'A' where id = 1
update table set name = 'B' where id = 1
commitupdate table set name = 'C' where id = 1
update table set name = 'D' where id = 1select name from table where id = 1 ------> read-view [1,3] 3
commit
commitselect name from table where id = 1 ------> read-view [1] 3

参考视频

在高并发情况下,如何做到安全的修改同一行数据?

  • 使用悲观锁
    悲观锁思想就是,当前线程要进来修改数据时,别的线程都得拒之门外~ 比如,可以使用select…for update 。
    因为悲观锁会影响系统吞吐的性能,所以适合应用在写为居多的场景下。

    select * from User where name=‘jay’ for update
    

    这条sql语句会锁定了User表中所有符合检索条件(name=‘jay’)的记录。本次事务提交之前,别的线程无法修改这些数据。

  • 使用乐观锁
    乐观锁思想就是,有线程过来,先放过去修改,如果看到别的线程没修改过,就可以修改成功,如果别的线程修改过,就修改失败或者重试。实现方式,乐观锁一般会使用版本号机制或者CAS算法实现。
    因为乐观锁就是为了避免悲观锁的弊端出现的,所以适合应用在读为居多的场景下。

select for update有什么含义,会锁表还是锁行还是其他?

  • select查询语句是不会加锁的,但是select for update除了有查询的作用外,还会加锁,而且它是悲观锁哦。至于加了是行锁还是表锁,这就要看是不是用了索引/主键啦。
  • 没用索引/主键的话就是表锁,否则就是是行锁。

锁的分类
在这里插入图片描述

死锁四要素

互斥条件、请求和保持条件、不剥夺条件、环路等待。

数据库死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。若无外力作用,事务都将无法推进下去。

  • 解决死锁问题最简单的一种方法是超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行。

  • 除了超时机制,当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测。较之超时的解决方案,这是一种更为主动的死锁检测方式。InnoDB存储引擎也采用的这种方式。wait-for graph要求数据库保存以下两种信息:

    • 锁的信息链表;

    • 事务等待链表;

通过上述链表可以构造出一张图,而在这个图中若存在回路,就代表存在死锁,因此资源间相互发生等待。这是一种较为主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通常来说InnoDB存储引擎选择回滚undo量最小的事务

共享锁与排他锁

InnoDB 实现了标准的行级锁,包括两种:共享锁(简称 s 锁)、排它锁(简称 x 锁)。

  • 共享锁(S锁):又叫做读锁。当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。
  • 排他锁(X锁):又叫做写锁。当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。

意向锁

  • 意向共享锁( IS 锁):事务想要获得一张表中某几行的共享锁
  • 意向排他锁( IX 锁):事务想要获得一张表中某几行的排他锁

记录锁(Record Locks)

  • 记录锁是最简单的行锁,仅仅锁住一行。如: SELECT c1 FROM t WHERE c1=10FOR UPDATE
  • 记录锁永远都是加在索引上的,即使一个表没有索引,InnoDB也会隐式的创建一个索引,并使用这个索引实施记录锁。
  • 会阻塞其他事务对其插入、更新、删除

间隙锁(Gap Locks)

  • 间隙锁是一种加在两个索引之间的锁,或者加在第一个索引之前,或最后一个索引之后的间隙。
  • 使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
  • 间隙锁只阻止其他事务插入到间隙中,他们不阻止其他事务在同一个间隙上获得间隙锁,所以 gap x lock 和 gap s lock 有相同的作用。

Next-Key Locks

  • Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。

插入意向锁(Insert Intention)

  • 插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,亦即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。
  • 假设有索引值4、7,几个不同的事务准备插入5、6,每个锁都在获得插入行的独占锁之前用插入意向锁各自锁住了4、7之间的间隙,但是不阻塞对方因为插入行不冲突。
兼容性SX
S兼容不兼容
X不兼容不兼容
MYSQL主从复制

原理
在这里插入图片描述

  1. 主库的更新事件(update、insert、delete)被写到binlog。
  2. 从数据库连接至主数据库。
  3. 主数据库创建一个binlog dump thread,把binlog的内容发送到从库。
  4. 从数据库创建一个IO线程,读取主数据库传过来的binlog内容并写入relay log。
  5. 从数据库还会创建一个SQL线程,从relay log里面读取内容,从ExecMasterLog_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db。

主从同步延迟的原因
由于从数据库读取binlog的线程仅有一个,当主库的并发较高时,产生的DML数量超过slave的SQL Thread所能处理的速度,或者当slave中有大型query语句产生了锁等待那么延时就产生了。

数据库连接池

连接池基本原理:在内部对象池中,维护一定数量的数据库连接,并对外暴露数据库连接的获取和返回方法。

优点:

  • 资源重用(连接复用)
  • 更快的系统响应速度
  • 新的资源分配手段
  • 统一的连接管理,避免数据库连接泄露。
SQL是如何执行的

基础架构

在这里插入图片描述

表结构设计
  • 三范式(防止冗余)

    • 字段具有原子性,即数据库表的每一个字段都是不可分割的原子数据项,不能是结合、数组、记录等非原子数据项,当实体中的某个属性有多个值时,必须拆分为不同的属性

    namemobileusernameaddress
    省、市、具体地址

    namemobileusernameprovincecityaddress
    • 满足1NF 的基础上,要求每一行数据具有唯一性,并且非主键字段完全依赖主键字段

    noclassnamescorecredit
    数学6

    noclassnamescore
    数学
    classnamecredit
    数学6
    • 满足2NF 的基础上,不能存在传递依赖
    idnoagesexschoolschool_addressschool_phone

    学校地址、电话依赖学校这个字段,学校依赖Id 这个字段

    schoolschool_addressschool_phone
  • 反模式设计:适当增加冗余(总是要多表查询➡单表查询)

  • 表设计原则

    • 字段少而精:把常用字段放在一起,不常用的字段独立出去,大字段独立出去
    • 尽量使用小型字段
    • 避免使用允许为NULL 的字段:允许为NULL 的字段很难查询优化,允许为NULL的字段的索引需要额外的空间
  • 合理平衡范式与冗余

  • 如果数据量非常大,考虑分库分表

联合查询
  • 内连接(inner join:取两张表中满足存在连接匹配关系的记录。
  • 外连接(outer join):取两张表中满足连接匹配关系的记录,以及某张表(或两张表)中不满足匹配关系的记录。
  • 笛卡尔连接(cross join):A、B两表的数据做任意连接,返回记录数等于两表记录数的乘积

嵌套循环连接NLJ

一次一行循环地从第一张表(称为驱动表)中读取行,在这行数据中取到关联字段,根据关联字段在另一张表(被驱动表)里取出满足条件的行,然后取出两张表的结果合集。

  • 驱动表:外层循环的表是驱动表(数据量比较小的一张表)
  • 被驱动表:内层循环的表是被驱动表(大表)

基于块的嵌套循环连接BNLJ

把驱动表的数据读入到 join_buffer 中,然后扫描被驱动表,把被驱动表每一行取出来跟 join_buffer 中的数据做对比。Join Buffer是基于内存的。

扫面次数的计算公式(S:驱动表的一行数据的大小,C:缓存的行数)
( S ∗ C ) / j o i n B u f f e r S i z e (S * C)/joinBufferSize SC/joinBufferSize

使用Join Buffer的条件:

  • 连接类型是ALL、INDEX、RANGE
  • 第一个nonconstant table 不会分配Join Buffer
  • Join Buffer只会缓存需要的字段,而非整行数据
  • 每个能够被缓存的join都会分配一个join buffer ,一个查询可能拥有多个join buffer
  • join buffer 在执行联接之前会分配,在查询完成后释放

Join调优原则

  • 原则: 减少磁盘IO次数
  • 用小表驱动大表:关联查询优化器会自动选择最优的执行顺序
    • straight_join可以强制先读左边的表
  • 如果有where条件,应该尽可能使用索引,并且尽可能减少外层循环的数据量(BNLJ扫描次数与外层表的记录数成正相关)
  • join的字段尽量创建索引
    • 当join字段的类型不同时,索引无法使用。
  • 尽量减少扫描的行数(百万以内)
  • 参与join的表不要太多
  • 如果被驱动表的join字段用不了索引,且内存较为充足,可以考虑把join buffer设置更大一些

Batched key access (BKA)

BNL和BKA都是批量的提交一部分结果集给下一个被join的表(标记为T),从而减少访问表T的次数。

Batched Key Access Join算法的工作步骤如下:

当索引的行满足条件时,不会马上去表中获取数据,而是把符合条件的索引存放到缓存中去,读取以下扫描到的索引,然后按照主键排序(非主键索引只会保存对应数据的主键,再根据主键去查找数据)B+树中的叶子节点都是按照主键顺序排列的。
在这里插入图片描述

  • 将外部表中相关的列放入Join Buffer中。
  • 批量的将Key(索引键值)发送到Multi-Range Read(MRR)接口。
  • Multi-Range Read(MRR)通过收到的Key,根据其对应的ROWID进行排序,然后再进行数据的读取操作(随机IO转顺序IO)。
  • 返回结果集给客户端。
SELECT * FROM SALARIES WHERE FROM_DATE <= '1980-01-01'
--使用范围查询时,尽管使用到了索引,可能会伴随大量的随机IO(寻道定位)===>数据是按照主键排列,不是字段排列的


SHOW VARIABLES LIKE '%optimizer_switch%'

--mrr=on,mrr_cost_based=on

--mrr缓存的大小
show variables like '%read_rnd_buffer_size%'


--mrr MRR的成本估算过于悲观。因此,mrr_cost_based也必须关闭才能使用BKA。
set optimizer_switch = 'mrr_cost_based=off'

set optimizer_switch = 'batched_key_access=on'
Having Where
  1. 一般情况下,WHERE 用于过滤数据行,而 HAVING 用于过滤分组。
  2. WHERE 查询条件中不可以使用聚合函数,而 HAVING 查询条件中可以使用聚合函数。
  3. WHERE 在数据分组前进行过滤,而 HAVING 在数据分组后进行过滤 。
  4. WHERE 针对数据库文件进行过滤,而 HAVING 针对查询结果进行过滤。也就是说,WHERE 根据数据表中的字段直接进行过滤,而 HAVING 是根据前面已经查询出的字段进行过滤。
  5. WHERE 查询条件中不可以使用字段别名,而 HAVING 查询条件中可以使用字段别名。
MySQL主从模式

主库为写库,从库为读库

主从延迟的来源

  • 有些部署条件下,从库所在机器的性能要比主库性能差。
  • 从库的压力较大,即从库承受了大量的请求。
  • 执行大事务。因为主库上必须等事务执行完成才会写入 binlog,再传给备库。如果一个主库上语句执行 10 分钟,那么这个事务可能会导致从库延迟 10 分钟。
  • 从库的并行复制能力。

解决主从延迟主要有以下方案

  • 配合 semi-sync 半同步复制(主库收到一个从库的以同步数据完成的ACK就返回此次事务执行成功);
  • 一主多从,分摊从库压力;
  • 强制走主库方案(强一致性);
  • sleep 方案:主库更新后,读从库之前先 sleep 一下;
  • 判断主备无延迟方案(例如判断 seconds_behind_master 参数是否已经等于 0、对比位点);
  • 从库并行复制。
故障恢复

使用数据库中间件MyCat来进行数据库故障恢复
MyCat

主数据库宕机

  1. 等待从库更新都执行完毕了
  2. 选择同步数据最多的从库作为主库(Exec_Master_Log_Pos最大)
  3. 把其他的从库指向新的主库
Mysql两阶段提交

用途:保证redolog、binlog日志之间的一致性。

在准备阶段,MySQL先将数据修改写入redo log,并将其标记为prepare状态,表示事务还未提交。然后将对应的SQL语句写入bin log。
在提交阶段,MySQL将redo log标记为commit状态,表示事务已经提交。然后根据sync_binlog参数的设置,决定是否将bin log刷入磁盘。

两阶段提交
  • 准备阶段(Prepare Phase):协调者询问所有参与者是否准备好提交事务。在准备阶段中,协调者会向所有参与者(Participant)节点发送准备请求,要求它们准备好事务的执行。在参与者节点执行完准备操作后,会向协调者发送准备完成的消息。协调者会在收到所有参与者的准备完成消息后,判断是否所有节点都准备好了,如果有一个节点没有准备好,那么协调者会发送回滚请求,要求所有节点回滚事务。
  • 提交阶段(Commit Phase):在提交阶段中,如果所有参与者都准备好了,那么协调者会向所有参与者发送提交请求,要求它们提交事务。在参与者节点执行完提交操作后,会向协调者发送提交完成的消息。协调者会在收到所有参与者的提交完成消息后,完成事务提交。
三阶段提交

TCC 将事务提交分为 Try(method1) - Confirm(method2) - Cancel(method3) 3个操作。其和两阶段提交有点类似,Try为第一阶段,Confirm - Cancel为第二阶段,是一种应用层面侵入业务的两阶段提交。
预处理 Try 、确认Confifirm 、撤销 Cancel 。 Try 操作做业务检查及资源预留, Confifirm 做业务确认操作, Cancel 实现一个与 Try 相反的操作即回滚操作。 TM 首先发起所有的分支事务的 try 操作,任何一个分支事务的 try 操作执行失败, TM 将会发起所有分支事务的 Cancel 操作,若 try 操作全部成功, TM 将会发起所有分支事务的 Confifirm 操作,其中 Confifirm/Cancel操作若执行失败, TM 会进行重试。

事务消息

在这里插入图片描述

Redis

Redis大厂面试题

Redis是一个基于内存的高性能键值对(key-value)的NoSQL数据库。可以用来做数据缓、会话保存、排行榜、好友推荐等。Redis支持事务、持久化、高可用等机制。

基本数据类型
  • String(字符类型)

    应用场景:分布式锁、缓存

    设置/获取单个值
    SET key value
    SET key
    TTL key
    
    同时设置/获取多个键值
    MSET key value [key value ....]
    MGET key [key ....]
    
    递增数字 可用于文章阅读量、点赞数和在看数
    INCR key
    
    增加指定的整数 
    INCRBY key increment
    
    递减数值
    DECR key
    
    减少指定的整数
    DECRBY key decrement
    
    获取字符串长度
    STRLEN key
    
    分布式锁
    set key value [Ex seconds][PX milliseconds][NX|XX]
    
    EX:key 在多少秒之后过期
    PX:key 在多少毫秒之后过期
    NX:当 key 不存在的时候,才创建 key,效果等同于setnx key value
    XX:当 key 存在的时候,覆盖 key
    
  • Hash(散列类型)
    redis 中的 hash 类似于 java 中的 Map<String,Map<Object,object>> 数据结构,即以字符串为 key,以 Map 对象为 value。
    应用场景:实现小型购物车。

    添加一个 hash 对象:
    HSET key field value
    
    获取 hash 对象的字段值:
    HGET key field
    
    添加多个 hash 对象:
    HMSET key field value [field value ...]
    
    获取多个 hash 对象的字段值:
    HMGET key field [field ....]
    
    获取 key 所对应所有的 hash 对象:
    HGETALL key
    	
    获取 key 对应的所有 hash 对象个数:
    HLEN key
    
    判断字段名为 field 的 hash 对象是否存在:
    HEXISTS key field
    
    删除一个 hash 对象:
    HDEL key
    
    如果 key 和 field 不存在,则初始值为 0,否则在之前的数值上递增:
    HINCRBY key field increment
    
  • List(列表类型)
    类似双端队列

    向 list 左边添加元素,如果 list 不存在则创建该 list:
    LPUSH key value [value ...]
    
    向 list 右边添加元素,如果 list 不存在则创建该 list:
    RPUSH key value [value ....]
    
    查看 list 中包含的元素:
    LRANGE key start stop
    注:LRANGE key 0 -1 表示查看 list 中所有的元素
    
    从左边出队:
    LPOP key
    
    从右边出队:
    RPOP key
    
    查看 list 中包含几个元素:
    LLEN key
    
  • Set(集合类型)

    应用场景:抽奖,共同关注,可能认识的人,商品推荐。

    set 中添加一个元素:
    SADD key member[member ...]
    
    删除 set 中的指定元素:
    SREM key member [member ...]
    
    获取 set 中的所有元素:
    SMEMBERS key
    
    判断元素是否在集合中
    SISMEMBER key member
    
    获取集合中的元素个数
    SCARD key
    
    从集合中随机弹出元素,元素不删除:
    SRANDMEMBER key [数字]
    
    从集合中随机弹出一个元素,出几个删几个:
    SPOP key[数字]
    
    集合的差集运算A-B:SDIFF key [key ...],属于A但不属于B的元素构成的集合
    
    集合的交集运算A∩B:SINTER key [key ...],属于A同时也属于B的共同拥有的元素构成的集合
    
    集合的并集运算AUB:SUNION key [key ...],属于A或者属于B的元素合并后的集合
    
  • SortedSet(有序集合类型,简称zset)

    形象理解 zset:向有序集合中加入一个元素和该元素的分数
    应用场景:排行榜、热搜榜

    实现原理
    压缩表 + 跳表在这里插入图片描述

    压缩表
    ziplist 编码的 Zset 使用紧挨在一起的压缩列表节点来保存,第一个节点保存 member,第二个保存 score。ziplist 内的集合元素按 score 从小到大排序,其实质是一个双向链表。
    在这里插入图片描述
    跳表

    • 链表 + 索引

    • 插入的时间复杂度为O(logn),每次插入都会先查找到要插入的位置(查找的时间复杂度就已经是【O(logn)】了,找到后直接插入【O(1)】,所以总的为【O(logn)】),删除也是同理为O(logn)

    • 每个节点的插入层次是通过getRandomLevel()随机出来的,插入层次互不影响 ,也被称为晋升机制。

    /**
     * 产生节点的高度。使用抛硬币
     *
     * @return
     */
    private int getRandomLevel() {
       //可知,元素的插入层次i从1开始自增,随机到哪一层的概率就像抛硬币一样,都是50%,故i越往后,其概率越小(每次都*0.5)
       //第一层概率:0.5,第二层0.5*0.5=0.25,...
          int lev = 1;
          while (random.nextInt() % 2 == 0) {
              lev++;
          }
          //MAX_LEVEL为跳表的最大层级
          return lev > MAX_LEVEL ? MAX_LEVEL : lev;
    }
    

在这里插入图片描述

  • 每一个节点的层数(level)是随机出来的,而且新插入一个节点不会影响其它节点的层数。因此,插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。这就降低了插入操作的复杂度。实际上,这是skiplist的一个很重要的特性,这让它在插入性能上明显优于平衡树的方案。 skiplist,指的就是除了最下面第1层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针故意跳过了一些节点(而且越高层的链表跳过的节点越多)。这就使得我们在查找数据的时候能够先在高层的链表中进行查找,然后逐层降低,最终降到第1层链表来精确地确定数据位置。在这个过程中,我们跳过了一些节点,从而也就加快了查找速度。

  • 查找节点时,从高索引层往低索引层查找: 一开始元素在高层从链表由前往后查找,直到要查找的目标元素在该层的某两个相邻元素之间,就会往下跳到下层的同一个位置,继续从同一位置向链表尾方向遍历查询->重复上面的过程,直到查找到目标元素 查找时每一层都跳过部分元素,进而加快了查找效率。

  • 与平衡树比较

    • 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
    • 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
    • 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
    向 zset 中添加一个带分数(权值)的元素:
    ZADD key score member [score member ...]
    
    删除 zset 中的指定元素:
    ZREM key member [member ...]
    
    返回索引从start到stop之间的所有元素,并按照元素分数从小到大的顺序:
    ZRANGE key start stop [WITHSCORES]
    注:如果想要获取所有元素并且从小到大排序,可写为 ZRANGE key 0 -1
    
    取指定元素的分数:
    ZSCORE key member
    
    获取指定分数范围的元素:
    ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
    
    获取集合中元素的数量:
    ZCARD key
    
    获得指定分数范围内的元素个数:
    ZCOUNT key min max
    
    增加某个元素的分数:
    ZINCRBY key increment member
    
    按照排名范围删除元素:
    ZREMRANGEBYRANK key start stop
    
    从小到大:
    ZRANK key member
    
    从大到小:
    ZREVRANK key member
    
  • Bitmap(位图)

  • HyperLogLog(统计)

  • GEO(地理)

Redis中的单线程与多线程

Redis的单线程指的是"其网络IO和键值对读写是由一个线程完成的"。也就是只有网络请求模块和数据操作模块是单线程的,而其他持久化存储模块、集群支撑模块等是多线程的。

Redis为什么不需要通过多线程的方式来提升IO利用率和CPU利用率?

  • 首先,不需要提高CPU利用率,因为redis的操作是基于内存的,CPU不是Redis的性能瓶颈。

  • Redis确实是一个IO密集型框架,数据操作过程中确实会有大量的网络IO和磁盘IO,提高IO利用率毋庸置疑。
    但是提高IO利用率,不是只有多线程一条路。

  • 多线程的弊端:线程安全、共享资源的并发控制;线程切换带来的性能开销。

  • redis采用的多路复用IO。(select、poll、epoll)

Redis的Hash 冲突
分布式锁
  • 使用当前请求的 UUID + 线程名作为分布式锁的 value。

  • stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value)

  • 方法尝试抢占锁 如果抢占失败,则返回值为 false;如果抢占成功,则返回值为 true。

  • 最后stringRedisTemplate.delete(REDIS_LOCK_KEY) 方法释放分布式锁。

  • 改进1:如果代码在执行的过程中出现异常,那么就可能无法释放锁,因此必须要在代码层面加上 finally 代码块,保证锁的释放

  • 改进2:假设部署了微服务 jar 包的服务器挂了,代码层面根本没有走到 finally 这块,也没办法保证解锁。这个 key 没有被删除,其他微服务就一直抢不到锁,因此我们需要加入一个过期时间限定的 key。

    stringRedisTemplate.expire(REDIS_LOCK_KEY, 10L, TimeUnit.SECONDS);
    
  • 改进三:加锁与设置过期时间的操作分开了,假设服务器刚刚执行了加锁操作,然后宕机了,也没办法保证解锁。证加锁和设置过期时间为原子操作。

    stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS)
    
  • 改进四:业务A删除了业务B的锁,只允许删除自己的锁,不允许删除别人的锁

    value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))
    

    在这里插入图片描述

  • 改进五:解锁原子版
    使用 lua 脚本保证解锁操作的原子性

/**
 * @ClassName GoodController
 * @Description TODO
 * @Author Oneby
 * @Date 2021/2/2 18:59
 * @Version 1.0
 */
@RestController
public class GoodController {

    private static final String REDIS_LOCK_KEY = "lockOneby";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/buy_goods")
    public String buy_Goods() throws Exception {
        // 当前请求的 UUID + 线程名
        String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
        try {
            // setIfAbsent() 就相当于 setnx,如果不存在就新建锁,同时加上过期时间保证原子性
            Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value, 10L, TimeUnit.SECONDS);

            // 抢锁失败
            if (lockFlag == false) {
                return "抢锁失败 o(╥﹏╥)o";
            }

            // 从 redis 中获取商品的剩余数量
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            String retStr = null;

            // 商品数量大于零才能出售
            if (goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                retStr = "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
            } else {
                retStr = "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
            }
            System.out.println(retStr);
            return retStr;
        } finally {
            // 获取连接对象
            Jedis jedis = RedisUtils.getJedis();
            // lua 脚本,摘自官网
            String script = "if redis.call('get', KEYS[1]) == ARGV[1]" + "then "
                    + "return redis.call('del', KEYS[1])" + "else " + "  return 0 " + "end";
            try {
                // 执行 lua 脚本
                Object result = jedis.eval(script, Collections.singletonList(REDIS_LOCK_KEY), Collections.singletonList(value));
                // 获取 lua 脚本的执行结果
                if ("1".equals(result.toString())) {
                    System.out.println("------del REDIS_LOCK_KEY success");
                } else {
                    System.out.println("------del REDIS_LOCK_KEY error");
                }
            } finally {
                // 关闭链接
                if (null != jedis) {
                    jedis.close();
                }
            }
        }
    }

}

Redisson

分布式锁可能存在锁过期释放,业务没执行完的问题。可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。

在这里插入图片描述

只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。

Redlock算法

Redis一般都是集群部署的,假设数据在主从同步过程,主节点挂了,分布式锁则失效。
在这里插入图片描述

如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。

在这里插入图片描述

Redlock核心思想:

多个Redis master部署,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。

实现步骤:

  • 按顺序向5个master节点请求加锁
  • 根据设置的超时时间来判断,是不是要跳过该master节点。
  • 如果大于等于三个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
  • 如果获取锁失败,解锁。
Redis事务

Redis通过MULTI、EXEC、WATCH等一组命令集合,来实现事务机制。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

Redis事务就是顺序性、一次性、排他性的执行一个队列中的一系列命令。

Redis执行事务的流程如下:

  • 开始事务(MULTI)
  • 命令入队
  • 执行事务(EXEC)、撤销事务(DISCARD )
缓存淘汰策略

过期键的删除策略(预防)

  • 定时删除
    • 创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作
    • 优点:节约内存,到时就删除,快速释放掉不必要的内存占用
    • 缺点:CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量
    • 总结:用处理器性能换取存储空间 (拿时间换空间)
  • 惰性删除
    • 数据到达过期时间,不做处理。等下次访问该数据时,如果未过期,返回数据 ;发现已过期,删除,返回不存在。
    • 优点:节约CPU性能,发现必须删除的时候才删除
    • 缺点:内存压力很大,出现长期占用内存的数据
    • 总结:用存储空间换取处理器性能(拿空间换时间)
  • 定期删除
    • 周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度
    • 特点1:CPU性能占用设置有峰值,检测频度可自定义设置
    • 特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
    • 总结:周期性抽查存储空间 (随机抽查,重点抽查)

内存淘汰策略(OOM时兜底方案)

8 种内存淘汰策略:两种维度(过期键中筛选、所有键中筛选),四种策略:(LRU 最近最少使用置换算法,LFU 最不经常使用置换算法,TTL Time To Live 生存时间,RANDOM 随机策略)

  • noeviction:不会驱逐任何key
  • allkeys-lru:对所有key使用LRU算法进行删除
  • volatile-lru:对所有设置了过期时间的key使用LRU算法进行删除
  • allkeys-random:对所有key随机删除
  • volatile-random:对所有设置了过期时间的key随机删除
  • volatile-ttl:删除马上要过期的key
  • allkeys-lfu:对所有key使用LFU算法进行删除
  • volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除

配置内存淘汰策略

  • 修改配置文件

    maxmemory-policy allkeys-lru
    
  • 通过命令修改(重启生效)

    设置内存淘汰策略
    config set maxmemory-policy allkeys-lru 
    
    获取当前采用的内存淘汰策略
    config get maxmemory-policy 
    
持久化机制

RDB

RDB(Redis DataBase缩写快照)是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。

在这里插入图片描述

  • 优点:

    • 适合大规模的数据恢复场景,如备份,全量复制等
  • 缺点

    • 没办法做到实时持久化/秒级持久化。
    • 新老版本存在RDB格式兼容问题

AOF

AOF(append only file) 持久化,采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的。

优点:

  • 数据的一致性和完整性更高

缺点:

  • AOF记录的内容越多,文件越大,数据恢复变慢。
Redis高可用

Redis 实现高可用有三种部署模式:主从模式,哨兵模式,集群模式

  • 主从模式
    主从模式中,Redis部署了多台机器,有主节点,负责读写操作,有从节点,只负责读操作。从节点的数据来自主节点,实现原理就是主从复制机制

    一般当slave第一次启动连接master,或者认为是第一次连接,就采用全量复制。
    在这里插入图片描述
    当master节点发生数据增减时,就会触发replicationFeedSalves()函数,接下来在 Master节点上调用的每一个命令会使用replicationFeedSlaves()来同步到Slave节点。执行此函数之前呢,master节点会判断用户执行的命令是否有数据更新,如果有数据更新的话,并且slave节点不为空,就会执行此函数。这个函数作用就是:把用户执行的命令发送到所有的slave节点,让slave节点执行。

  • 哨兵模式
    哨兵模式,由一个或多个Sentinel实例组成的Sentinel系统,它可以监视所有的Redis主节点和从节点,并在被监视的主节点进入下线状态时,自动将下线主服务器属下的某个从节点升级为新的主节点。但是呢,一个哨兵进程对Redis节点进行监控,就可能会出现问题(单点问题),因此,可以使用多个哨兵来进行监控Redis节点,并且各个哨兵之间还会进行监控。
    在这里插入图片描述
    哨兵作用:

    • 发送命令,等待Redis服务器(包括主服务器和从服务器)返回监控其运行状态;
    • 哨兵监测到主节点宕机,会自动将从节点切换成主节点,然后通过发布订阅模式通知其他的从节点,修改配置文件,让它们切换主机;
    • 哨兵之间还会相互监控,从而达到高可用。

    工作模式:

    • 每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他Sentinel实例发送一个 PING命令。
    • 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel标记为主观下线。
    • 如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。
    • 当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线。
    • 在一般情况下, 每个 Sentinel 会以每10秒一次的频率向它已知的所有Master,Slave发送 INFO 命令。
    • 当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次
    • 若没有足够数量的 Sentinel同意Master已经下线, Master的客观下线状态就会被移除;若Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。
Cluster集群模式

哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。因此,Cluster集群应运而生,它在Redis3.0加入的,实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。并且,它也提供复制和故障转移的功能。

集群通信方式

Redis Cluster集群通过Gossip协议进行通信,节点之前不断交换信息,交换的信息内容包括节点出现故障、新节点加入、主从节点变更信息、slot信息等等。常用的Gossip消息分为4种,分别是:ping、pong、meet、fail

在这里插入图片描述

  • meet消息:通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
  • ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。
  • pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
  • fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。

每个节点是通过集群总线(cluster bus) 与其他的节点进行通信的。通讯时,使用特殊的端口号,即对外服务端口号加10000。例如如果某个node的端口号是6379,那么它与其它nodes通信的端口号是 16379。nodes 之间的通信采用特殊的二进制协议。

Hash Slot插槽算法

插槽算法把整个数据库被分为16384个slot(槽),每个进入Redis的键值对,根据key进行散列,分配到这16384插槽中的一个。使用的哈希映射也比较简单,用CRC16算法计算出一个16 位的值,再对16384取模。数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点都可以处理这16384个槽。

集群中的每个节点负责一部分的hash槽,比如当前集群有A、B、C个节点,每个节点上的哈希槽数 =16384/3,那么就有

  • 节点A负责0~5460号哈希槽
  • 节点B负责5461~10922号哈希槽
  • 节点C负责10923~16383号哈希槽

Redis Cluster集群中,需要确保16384个槽对应的node都正常工作,如果某个node出现故障,它负责的slot也会失效,整个集群将不能工作。

因此为了保证高可用,Cluster集群引入了主从复制,一个主节点对应一个或者多个从节点。当其它主节点 ping 一个主节点 A 时,如果半数以上的主节点与 A 通信超时,那么认为主节点 A 宕机了。如果主节点宕机时,就会启用从节点。

在Redis的每一个节点上,都有两个玩意,一个是插槽(slot),它的取值范围是0~16383。另外一个是cluster,可以理解为一个集群管理的插件。当我们存取的key到达时,Redis 会根据CRC16算法得出一个16 bit的值,然后把结果对16384取模。每个key都会对应一个编号在 0~16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。

虽然数据是分开存储在不同节点上的,但是对客户端来说,整个集群Cluster,被看做一个整体。客户端端连接任意一个node,看起来跟操作单实例的Redis一样。当客户端操作的key没有被分配到正确的node节点时,Redis会返回转向指令,最后指向正确的node,这就有点像浏览器页面的302 重定向跳转。

在这里插入图片描述

故障转移

Redis集群实现了高可用,当集群内节点出现故障时,通过故障转移,以保证集群正常对外提供服务。

redis集群通过ping/pong消息,实现故障发现。这个环境包括主观下线和客观下线。

故障发现

  • 主观下线: 某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。

    在这里插入图片描述

  • 客观下线: 指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。

    假如节点A标记节点B为主观下线,一段时间后,节点A通过消息把节点B的状态发到其它节点,当节点C接受到消息并解析出消息体时,如果发现节点B的pfail状态时,会触发客观下线流程;
    当下线为主节点时,此时Redis Cluster集群为统计持有槽的主节点投票,看投票数是否达到一半,当下线报告统计数大于一半时,被标记为客观下线状态。

    在这里插入图片描述

故障恢复:故障发现后,如果下线节点的是主节点,则需要在它的从节点中选一个替换它,以保证集群的高可用。

  • 资格检查:检查从节点是否具备替换故障主节点的条件。

  • 准备选举时间:资格检查通过后,更新触发故障选举时间。

  • 发起选举:到了故障选举时间,进行选举。

  • 选举投票:只有持有槽的主节点才有票,从节点收集到足够的选票(大于一半),触发替换主节点操作。

    在这里插入图片描述

Redis缓存

在这里插入图片描述
缓存雪崩

Redis中的缓存数据是有过期时间的,当在同一时间大量的缓存同时失效时就会造成缓存雪崩。

解决方案:

  • 永不过期
  • 合理的设置过期时间
  • 并发量不高的时候可以使用分布式锁(加锁排队限流)

缓存击穿
缓存击穿和缓存雪崩类似,也是因为Redis中key过期导致的。只不过缓存击穿是某一个热点的key过期导致的。当有一个热点数据突然过期时,就会导致突然有大量的情况直接落到MySql上,导致MySql直接爆炸!

解决方案:

  • 永不过期
  • 设置较长的过期时间
  • 使用Redis的分布式锁

缓存穿透

Redis缓存穿透指的是,在Redis缓存和数据库中都找不到相关的数据。也就是说这是个非法的查询,客户端发出了大量非法的查询 比如id是负的 ,导致每次这个查询都需要去Redis和数据库中查询。导致MySql直接爆炸!

解决方案:

  • 过滤非法查询

  • 缓存空对象

  • 布隆过滤器

    • 布隆过滤器由一个二进制数组和k个哈希数组组成。
    • 当我们想查询一个元素时(例如查询python),布隆过滤器就会使用一系列随机映射函数计算出多个索引值,然后查询二进制数组中的对应位置是否都为1。如果都为1就说明改元素存在。但是布隆过滤器存在误判的可能性,因为不同的元素hash后的值可能是一样的。
    • 存在误判的可能性
    • 返回false代表数据一定不存在
    • 查询的时间复杂度是O(k),k为hash函数个数
    • k越大,数组长度越大,误判的可能性越低
    • 使用位图(二进制数组)所以内存压力较小
    • 布隆数据预热
    public String guavaBloomTestSet(User user){
        //插入数据
        userMapper.insert(user);
        //加到布隆过滤器
        bloomFilter.put(user.getId());
     
        return "success";
    }
    
    • 布隆过滤器过滤非法数据处理
    public User guavaBloomTestGet(@PathVariable Integer id){
     
        //判断布隆过滤器是否存在
        if(bloomFilter.mightContain(id)){
     
            //获取缓存数据
            String s = stringRedisTemplate.opsForValue().get(PREFIX+id.toString());
     
            if(s==null){
                //缓存不存在,查询数据库
                User user = userMapper.selectById(id);
                //空值处理(布隆误判 | 数据被删)
                String value=user==null?"":JSONUtil.toJsonStr(user);
                //加入缓存
                stringRedisTemplate.opsForValue().set(PREFIX+id.toString(), value, Duration.ofSeconds(300L));
                return user;
            }else if(s.equals("")){
                //缓存空值处理
                return null;
            }else {
     
                return JSONUtil.toBean(s,User.class);
            }
     
        }
     
        //不存在布隆过滤器直接返回
        return null;
    }
    
    • 布隆过滤器拦截一些不在业务之内的非法请求,从而达到保护服务的作用
脑裂

什么是redis的集群脑裂?

redis的集群脑裂是指因为网络问题,导致redis master节点跟redis slave节点和sentinel集群处于不同的网络分区,此时因为sentinel集群无法感知到master的存在,所以将slave节点提升为master节点。此时存在两个不同的master节点,就像一个大脑分裂成了两个。
集群脑裂问题中,如果客户端还在基于原来的master节点继续写入数据,那么新的master节点将无法同步这些数据,当网络问题解决之后,sentinel集群将原先的master节点降为slave节点,此时再从新的master中同步数据,将会造成大量的数据丢失。

解决:
第一个参数表示连接到master的最少slave数量
第二个参数表示slave连接到master的最大延迟时间
按照上面的配置,要求至少3个slave节点,且数据复制和同步的延迟不能超过10秒,否则的话master就会拒绝写请求,配置了这两个参数之后,如果发生集群脑裂,原先的master节点接收到客户端的写入请求会拒绝,就可以减少数据同步之后的数据丢失。

在这里插入图片描述

Linux

select poll epoll

这三个都是多路复用方面的技术,而多路复用指的是多个 socket 复用同一个线程

socket描述符(FD)
在这里插入图片描述
epoll可以理解为event pool,不同与select、poll的轮询机制,epoll采用的是事件驱动机制,每个fd上有注册有回调函数,当网卡接收到数据时会回调该函数,同时将该fd的引用放入rdlist就绪列表中。

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

epoll数据结构

struct eventpoll {
  ...
  /*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,
  也就是这个epoll监控的事件*/
  struct rb_root rbr;
  /*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
  struct list_head rdllist;
  ...
};

我们在调用 epoll_create 时,内核除了帮我们在 epoll 文件系统里建了个 file 结点,在内核 cache 里建了个红黑树用于存储以后 epoll_ctl 传来的 socket 外,还会再建立一个 rdllist 双向链表,用于存储准备就绪的事件,当 epoll_wait 调用时,仅仅观察这个 rdllist 双向链表里有没有数据即可。

epoll执行流程

  • 调用epoll_create()创建一个ep对象,即红黑树的根节点,返回一个文件句柄

  • 调用epoll_ctl()向这个ep对象(红黑树)中添加、删除、修改感兴趣的事件
    为什么选红黑树?
    epoll在内核中维护了一个内核事件表,它是将所有的文件描述符全部都存放在内核中,系统去检测有事件发生的时候触发回调,当你要添加新的文件描述符的时候也是调用epoll_ctl函数使用EPOLL_CTL_ADD宏来插入,epoll_wait也不是每次调用时都会重新拷贝一遍所有的文件描述符到内核态。当我现在要在内核中长久的维护一个数据结构来存放文件描述符,并且时常会有插入,查找和删除的操作发生,这对内核的效率会产生不小的影响,因此需要一种插入,查找和删除效率都不错的数据结构来存放这些文件描述符,那么红黑树当然是不二的人选。

  • 调用epoll_wait()等待,当有事件发生时网卡驱动会调用fd上注册的函数并将该fd添加到rdlist中,解除阻塞

LT和ET两种触发模式

是默认的模式,ET是“高速”模式。

LT(水平触发)模式下,只要这个文件描述符还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作;

ET(边缘触发)模式下,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。如果ET模式不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。

在这里插入图片描述

红黑树

红黑树是一种二叉搜索树

  • 每个结点不是红色就是黑色
  • 根节点是黑色的
  • 如果一个节点是红色的,则它的两个孩子结点是黑色的
  • 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
  • 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)

通过任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

红黑树不追求绝对平衡,只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
在这里插入图片描述

闭包

闭包(Closure)是一种函数(或者是匿名函数)和与其相关的引用环境组合的对象。闭包可以访问在定义它的外部作用域中的变量,即使这些变量在闭包被调用时已经不再存在。

function outer() {
  var x = 1;
  function inner() {
    console.log(x);
  }
  return inner;
}

var closure = outer();  // 调用 outer 函数,返回 inner 函数
closure();  // 输出 1

RocketMQ消息投递的顺序性

对事务的Id进行hash,通过这个hash值将这个消息投递到指定的队列中。
发送全局顺序消息

Git Merge Rebase

区别

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值