一.计算机网络
1.网络基础知识讲解
OSI开放式互联参考模型
第1层 物理层
首先解决两台物理机之间的通信需求,具体就是机器A往机器B发送比特流,机器B能收到比特流。
物理层主要定义了物理设备的标准,如网线的类型,光纤的接口类型,各种传输介质的传输速率。主要作用是传输比特流(0101二进制数据),将比特流转化为电流强弱传输,到达目的后再转化为比特流,即常说的数模转化和模数转换。这层数据叫做比特。网卡工作在这层。
第2层 数据链路层
在传输比特流的过程中,会产生错传、数据传输不完整的可能。
数据链路层定义了如何格式化数据进行传输,以及如何控制对物理介质的访问。通常提供错误检测和纠正,以确保数据传输的准确性。本层将比特数据组成帧,交换机工作在这层,对帧解码,并根据帧中包含的信息把数据发送到正确的接收方。
第3层 网络层
随着网络节点的不断增加,点对点通讯需要通过多个节点,如何找到目标节点,如何选择最佳路径成为首要需求。
网络层主要功能是将网络地址转化为对应的物理地址,并决定如何将数据从发送方路由到接收方。网络层通过综合考虑发送优先权、网络拥塞程度、服务质量以及可选路由的花费来决定从一个网络中节点A到另一个网络中节点B的最佳路径。由于网络层处理并智能指导数据传送,路由器连接网络隔断,所以路由器属于网络层。此层的数据称之为数据包。本层需要关注的协议TCP/IP协议中的IP协议。
第4层 传输层
随着网络通信需求的进一步扩大,通信过程中需要发送大量的数据,如海量文件传输,可能需要很长时间,网络在通信的过程中会中断很多次,此时为了保证传输大量文件时的准确性,需要对发送出去的数据进行切分,切割为一个一个的段落(Segement)发送,其中一个段落丢失是否重传,段落是否按顺序到达,是传输层需要考虑的问题。
传输层解决了主机间的数据传输,数据间的传输可以是不同网络,并且传输层解决了传输质量的问题。传输协议同时进行流量控制,或是基于接收方可接收数据的快慢程度规定适当的发送速率。除此之外,传输层按照网络可处理的最大尺寸将较长的数据包进行强制分割,例如以太网无法接收大于1500字节的数据包,发送方节点的传输层将数据分割成较小的数据片并编号,以便数据到达接收方节点的传输层时能以正确的顺序重组,该过程称为排序。传输层需要关注的协议有TCP/IP协议中的TCP协议和UDP协议。
第5层 会话层
自动收发包,自动寻址。
会话层作用是建立和管理应用程序间的通信。
第6层 表示层
Linux给WIndows发包,不同系统语法不一致,如exe不能在Linux下执行,shell不能在Windows不能直接运行。于是需要表示层。
解决不同系统之间通信语法问题,在表示层数据将按照网络能理解的方案进行格式化,格式化因所使用网络的不同而不同。
第7层 应用层
规定发送方和接收方必须使用一个固定长度的消息头,消息头必须使用某种固定的组成,消息头中必须记录消息体的长度等信息,方便接收方正确解析发送方发送的数据。应用层旨在更方便应用从网络中接收的数据,重点关注TCP/IP协议中的HTTP协议。
从应用层开始对要传输的数据头部进行处理,加上本层的一些信息。最终物理层通过以太网、电缆等介质将数据解析成比特流在网络中传输,数据传递到目标地址,自底而上的将先前对应的头部解析分离出来。
OSI参考模型各层的主要功能?
2.TCP的三次握手
TCP/IP
OSI的实现“TCP/IP”
IP、TCP、UDP、HTTP等都属于TCP/IP协议,TCP/IP泛指这些协议。
OSI模型注重通信协议必要的功能;TCP/IP更强调在计算机上实现协议应该开发哪种程序。
IP协议是无连接的通信协议,不会占用两个正在通信的计算机之间的通信线路。降低了对网络线路的需求,每条线同时满足许多不同计算机之间的通信需要。通过IP,消息或其他数据被分割为较小的独立的包,并通过因特网在计算机之间传送。IP负责将每个包路由至目的地。IP协议没有确认包是否按顺序发送,或包是否被破环,因此IP数据包是不可靠的。需要它的上层协议做出控制。
传输控制协议TCP简介
1)面向连接的、可靠的、基于字节流的传输层通信协议
2)将应用层的数据流分割成报文段并发送给目标节点的TCP层
3)数据包都有序号,对方收到则发送ACK确认,未收到则重传
4)使用校验和来检验数据在传输过程中是否有误参考:百度百科_TCP_简介
应用层向TCP层发送数据流,然后TCP把数据流分区成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元MTU的限制)。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认ACK;如果发送端实体在合理的往返时延RTT内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
参考:TCP分段与IP分片
TCP报文头
- 端口:两个进程在计算机内部进行通信,有管道、内存共享、信号量、消息队列等方法。两个进程通信最基本的前提是唯一标识一个进程,通过唯一标识找到对应的进程。在本地进程通信中可以通过pid,但pid只在本地唯一。如果把两个进程放到两台计算机通信,pid实现不了。解决这个问题的方法就是在传输层使用协议端口号,简称端口。IP地址可以唯一标识主机,TCP协议和端口号可以唯一标识主机中的一个进程,利用IP地址+协议+端口号唯一标识去标识网络中的一个进程,这种唯一标识的模式称为Socket。
- seq序号:占4字节,TCP连接中传送的字节流中的每个字节都按顺序编号。例如:一段报文的序号字段值是107,携带的数据是100个字段,下一个报文段序号从107+100=207开始。
- ack确认号:4个字节,是期望收到对方下一个报文段的第一个数据字节的序号。例如:B收到A发送的报文,其序号字段是301,数据长度是200字节,表明B正确收到A发送的到序号500为止的数据(301+200-1=500),B期望收到A下一个数据序号是501。B发送给A的确认报文段中把ack确认号置为501。
- 数据偏移:头部有可选字段,长度不固定,指出TCP报文段的数据起始处距离报文段的起始处有多远。
- 保留:保留今后使用的,被标为1。
- 窗口:滑动窗口大小,用来告知发送端接收端缓存大小,以此控制发送端发送数据的速率,从而达到流量控制。
- 校验和:奇偶校验,此校验和是对整个的TCP报文段(包括TCP头部和TCP数据),以16位进行计算所得,由发送端计算和存储,接收端进行验证。
- 紧急指针:只有控制位中的URG为1时才有效。指出本报文段中的紧急数据的字节数。
- 选项:其长度可变,定义其他的可选参数。
- 控制位:由8个标志位组成。每个标志位表示一个控制功能。
其中主要的6个:
- URG紧急指针标志,为1表示紧急指针有效,为0忽略紧急指针。
- ACK确认序号标志,为1表示确认号有效,为0表示报文不含确认信息,忽略确认号字段。上面的确认号是否有效就是通过该标识控制的。
- PSHpush标志,为1表示带有push标志的数据,指示接收方在接收到该报文段以后,应尽快将该报文段交给应用程序,而不是在缓冲区排队。
- RST重置连接标志,重置因为主机崩溃或其他原因而出现错误的连接,或用于拒绝非法的报文段或非法的连接。
- SYN同步序号,用于建立连接过程,在连接请求中SYN=1和ACK=0表示该数据段没有使用捎带的确认域,连接应答捎带一个确认即SYN=1和ACK=1。
- FIN终止标志,用于释放连接,为1时表示发送方没有发送了。
TCP三次握手
- 当应用程序希望通过TCP与另一个应用程序通信时,会发送一个通信请求,这个请求必须发送到一个确切的地址,双方握手之后,TCP建立一个全双工的通信(计算机A能给B发送信息,在发送信息的同时B也能给A回发信息。半双工通信:双向交替通信,即通信的双方都可以发送信息,但不能双方同时发送和接收),这个通信将占用两个计算机之间的通信线路,直到它被一方或双方关闭为止。握手即为TCP三次握手。
- 首先假设A和B首次通信,客户端和服务器端处于CLOSED的状态,假设主动打开连接的是客户端,被动打开连接的是服务器端。TCP服务器进程先创建传输控制块TCB,时刻准备接收其他客户进程发送的连接请求,此时服务端进入Listen即监听状态。TCP客户端进程创建传输控制块TCB,向服务器发出连接请求报文,TCP请求报文中的同步序号SYN=1,同时选择一个初始序号seq=x,x为任意的正整数值。TCP客户端进程进入SYN-SENT同步已发送状态,发送过去的报文段称为SYN报文段,不携带数据,但消耗掉一个序号,这便是第一次握手。
- 当服务器接收到请求报文后,如果同意连接,则发出确认报文,确认报文中包含ACK=1,SYN=1,确认号ack=x+1(SYN报文中指定seq=x,作为回应回应和x相关的信息,上面的报文消耗掉一个序号,因此为x+1),同时为自己的缓存初始化一个序列号即seq=y。服务器进入SYN-RCVD即同步收到的状态,这个报文也不携带数据,同样消耗一个序号,这便是第二次握手。
- TCP客户进程收到确认报文后还要向服务器给出一个确认,确认报文ACK=1,ack=y+1(服务器给发了seq=y,作为回应ack,同时报文消耗一个序号seq,回应就是y+1),先前告知序号seq已经被加1了,seq=x+1。此时TCP连接建立,客户端进入ESTAB-LISHED建立连接的状态,TCP规定这个ACK报文段可以携带数据(前两个不携带),也可以不携带,不携带就不会消耗序号,这便是第三次握手。服务器收到客户端的确认后,也会进入ESTAB的状态,双方就可以通信了。
简介如下:
TCP 为什么三次握手而不是两次握手?
在前两次握手的时候双方都随机选择了自己的初始段序号,并且第二次握手的时候连接请求发送端收到了自己的ack number,确认了自己的序列号,而连接请求响应端还没有确认自己的序列号,没有收到ack number。 如果这时候两次握手下就进行数据传递, 序号没有同步,数据就会乱序。
参考 TCP 为什么三次握手而不是两次握手、彻底搞懂TCP协议:从 TCP 三次握手四次挥手说起
3.TCP的四次挥手
挥手即终止TCP连接,即断开一个TCP连接时,需要客户端和服务端总共发出四个包,以确认连接的断开。在Socket编程中,这一过程由客户端或服务端任一方执行CLOSE来触发。假设由客户端主动促发CLOSE。
开始时客户端和服务端都处于ESTAB的状态,客户端主动关闭,服务器被动关闭。首先客户端发出连接释放报文,并且停止发送数据,FIN=1,假设客户端定义的序列号seq=u(该值等于前面ESTAB状态下数据最后一次发送时已经传送过来数据的最后一个序号+1)。此时客户端进入FIN-WAIT-1终止等待1的状态,TCP规定即使FIN报文不携带数据也要消耗掉一个序号,即回执时u+1。当服务器收到连接释放报文之后发出确认报文ACK=1,作为回应ack=u+1,携带上自己的序列号seq=v。服务端进入CLOSE-WAIT关闭等待的状态。TCP服务器通知高层的应用进程,客户端要释放跟服务器通信的连接,这时处于半关闭的状态,即客户端已经没有数据发送,服务器要发送数据,客户端还可以接收,这个状态持续一段时间,该时间等于CLOSE-WAIT所持续的时间。客户端收到服务器的确认请求后即第二次挥手时客户端进入FIN-WAIT2即终止等待2状态,等待服务器发送释放连接报文,即等待发送第三次挥手的请求。在这段时间可能接收服务器发送的最后的数据,服务器将最后的数据发送完毕后,向客户端发送连接释放报文,FIN=1,ACK=1,ack=u+1,在半关闭的状态,服务器可能发送了一些数据,假设seq变为w,此时服务器进入LAST-ACK即最后确认状态,等待客户端的最终确认。客户端在收到服务器的连接释放报文后必须发出确认即ACK=1,ack=w+1回发回去,自己的序号按之前报文 的序号加1即seq=u+1,客户端进入TIME-WAIT时间等待状态,此时客户端的TCP连接还没有释放,必须经过2*MSL时间后连接才真正释放,进入CLOSE状态。MSL即最长报文段寿命。服务器只要收到客户端发出的确认,立即进入CLOSE的状态。服务器结束TCP连接时间比客户端稍早些。
为什么会有TIME_WAIT状态?为什么不直接为CLOSE状态?
- 确保有足够的时间让对方收到ACK包(被动关闭的一方没有收到ACK,会触发被动端重发FIN包,一来一回就是2MSL)
- 避免新旧连接混淆(有些路由器会缓存IP数据包,如果连接被重用,延迟收到的包有可能会跟新连接混在一起)
为什么需要四次挥手才能断开连接?
因为TCP是全双工,发送方和接收方都需要FIN报文和ACK报文。发送方和接收方各需两次挥手,一方是被动的,看上去就是四次挥手。
服务器出现大量CLOSE_WAIT状态的原因?
其中一个表现是客户端一直在请求,返回给客户端的信息是异常的,服务端没有收到请求。
对方关闭socket连接,我方忙于读或写,没有及时关闭连接检查代码,特别是释放资源的代码
检查配置,特别是处理请求的线程配置
4.TCP和UDP的区别
UDP用户数据报协议,相比TCP报文,UDP报文的域少了很多,简单很多
UDP的特点
- 面向非连接(传输数据之前,源端和终端不建立连接。当它想传送时,就简单抓取来自应用程序的数据,并尽可能快的把它扔到网络上。在发送端,UDP传送数据的速度仅仅受应用程序生成数据的速度、计算机能力、传输带宽的限制。在接收端,UDP把每个消息段放入队列中,应用程序每次从队列中读取一个消息段。)
- 不维护连接状态,支持同时向多个客户端传输相同的消息
- 数据包报头只有8个字节,额外开销较小
- 吞吐量只受限于数据生成速率、传输速率以及机器性能
- 尽最大努力交付,不保证可靠交付,不需要维持复杂的链接状态表
- 面向报文,不对应用程序提交的报文信息进行拆分或者合并
TCP和UDP的区别
TCP提供可靠的通信传输,UDP常被用于让广播和细节控制交给应用层的通信传输
区别如下:
- 面向连接VS无连接
TCP面向连接,UDP面向无连接。TCP有三次握手的连接过程;UDP适合消息的多播发布,从单个点向多个点传输信息。- 可靠性
TCP可靠,利用握手确认和重传机制提供可靠性保证;UDP可能丢失,不知道到底有没有接收。- 有序性
TCP利用序列号保证消息报的顺序交付,到达可能无序,但TCP最终排序;UDP不具备有序性- 速度
TCP速度慢,因为需要创建连接,保证消息的可靠性、有序性,需要做额外的操作;UDP更适合对速度比较敏感的应用,比如在线视频媒体、电视广播、多人在线游戏。- 量级
TCP属于中级,UDP属于轻量级。体现在源数据的头大小,TCP是20个字节,UDP是8个字节。
TCP和UDP适用场景
参考: TCP、UDP的区别和适用场景
5.流量控制和拥塞控制
5.1 RTT和RTO
RTT:发送一个数据包到收到对应的ACK,所花费的时间
RTO:重传时间间隔(TCP在发送一个数据包后会启动一个重传定时器。RTO即定时器的重传时间)开始预先算一个定时器时间,如果回复ACK,重传定时器就自动失效,即不需要重传;如果没有回复ACK,RTO定时器时间就到了,重传。RTO是本次发送当前数据包所预估的超时时间,RTO不是固定写死的配置,是经过RTT计算出来的。基于RTO便有了重传机制。
5.2 TCP的滑动窗口(流量控制)
TCP将数据拆分成段进行发送,出于效率和传输速率的考虑,不可能一段一段数据发送,等上一段数据确认后再发送下一段数据,效率低。要实现对数据的批量发送,TCP解决可靠传输和包乱序的问题,所以TCP需要知道网络实际的数据处理带宽或数据处理速度,这样才不会引起网络拥塞导致丢包。
TCP使用滑动窗口做流量控制与乱序重排。
TCP的滑动窗口主要有两个作用:
- 保证TCP的可靠性
- 保证TCP的流控特性
TCP报文头有个字段叫Window,用于接收方通知发送方自己还有多少缓存区可以接收数据,发送方根据接收方的处理能力来发送数据,不会导致接收方处理不过来,这便是流量控制。
窗口数据的计算过程:
- 上图左面是TCP的发送端缓冲区,右面是接收端缓冲区,左面往右边发送数据。下面的长方形表示要发送的数据流,里面假设装满数据,并且按照顺序从左向右发送或者接收。假设对应的数据段位置序号也是从左到右。
- 对于发送方LastByteAcked指向收到的连续最大的ACK的位置,即从左端算起连续已经被接收程序发送ACK回执确认已收到的seq num。LastByteSent指向已发送的最后一个字节的位置,该位置只是发出去了,还没收到ACK的回应。LastByteWritter指向上层应用已写完的最后一个字节的位置,即当前程序已经准备好的需要发送的最新的数据段。从LastByteAcked到LastByteSent是发送出去还没收到确认的,LastByteAcked之前是已经发送出去并且收到接收端确认的。
- 对于接收方LastByteRead指向上层应用已经读完的最后一个字节的位置,即收到发送方数据,已经处理并且回执数据的最后一个位置。NextByteExpected指向收到的连续最大的seq的位置。从LastByteRead到NextByteExpected是已经收到还没发送回执。LastByteRecvd是已收到的最后一个字节的位置。NextByteExpected到LastByteRecvd有些seq还没有到达,对应空白区域。可以根据上面的数值计算出接收方AdvertisedWindow的大小,回发给发送方让其计算出发送方的剩余和发送的数据大小,即EffectiveWindow 的大小。
- 接收方还能处理的数据的量AdvertisedWindow = MaxRcvBuffer - (LastByteRcvd - LastByteRead)
- MaxRcvBuffer:接收方能接收的最大数据量,也可以理解为接收端缓存池的大小。
- LastByteRcvd-LastByteRead表示的是当前接收方已为接收到的数据或还没有接收到的预定的数据留出来的空间。当前这些空间已经占据一定的缓存,用最大的缓存减去已经占据的缓存得出还能够接收的数据量。进而可以将AdvertisedWindow告知发送端。发送方根据ACK中AdvertisedWindow的值需要保证LastByteSent-LastByteAcked<=AdvertisedWindow,即已发送且带确认的数据量小于接收方的windows大小
- 窗口内剩余可发送数据的大小EffectiveWindow = AdvertisedWindow - (LastByteSent - LastByteAcked)
- LastByteSent-LastByteAcked是发送出去待确认的,以接收方能够承受的数据量AdvertisedWindow为基准,当前已经发了 LastByteSent-LastByteAcked还没有被接收,剩下的空间是还能够发送的数据量的大小。
滑动窗口的基本原理
- TCP会话的发送方
在其发送缓存内的数据可以分为4类:已经发送并且得到端的回应的、已经发送但还没收到端的回应的、未发送但对端允许发送的、未发送且由于达到window的大小对端不允许发送的。已经发送但还没收到对端ACK确认的、未发送但对端允许发送的组成滑动窗口。
当收到接收方新的ACK对于发送窗口中后序字节的确认时,窗口就会滑动。滑动原理如下:假设原滑动窗口边界是32到51,假设已发送但还未被确认的序号是32到40。如果32和33没被确认,34被确认,这个窗口不会向右滑动,只有等到32到34都被确认之后即连续被确认之后滑动窗口才会被移动。在没被移动之前,序号>=52的数据即窗口外的数据是不能被发送的。假设从32到35都被确认了,滑动窗口会向右移动4位到36,进而程序能够发送52到55的数据。
- TCP会话的接收方
某一时刻在接收缓存内会存在三种状态:已接收已发送回执的状态,未接收但可以接收(准备接收)的状态、未接收不能接收的状态(达到窗口的阈值不能接收)。由于ACK直接由TCP栈回复,默认没有应用延迟,不存在已接收但未回复ACK的状态。未接收准备接收的空间称为接收窗口。TCP传输可靠性来自确认重传机制,TCP滑动窗口的可靠性也建立在确认重传机制的基础上。发送窗口只有收到接收端对于本段发送窗口内字节的ACK确认,才会移动发送窗口的左边界,接收窗口只有在前面所有的段都确认的情况下才会移动左边界。当前面还有字节未接收但收到后面字节的情况下窗口不会移动,并不对后续字节确认,以此确保对端会对这些数据重传。
滑动窗口的大小可以根据一定策略动态调整,应用会根据自身处理能力的变化,通过本端TCP接收窗口的大小控制来实现对端的发送窗口进行流量限制。
拥塞控制
参考:计算机网络:流量控制和拥塞控制、TCP流量控制和拥塞控制、TCP流量控制和拥塞控制、TCP的阻塞和重传机制
6.HTTP相关
超文本传输协议HTTP主要特点
HTTP是基于请求与响应模式的无状态的应用层协议。绝大多的web开发是构建在HTTP协议上的web应用。
特点:
- 支持客户/服务器模式:浏览器作为HTTP客户端通过URL向HTTP服务端即web服务器发送请求,web服务器根据接收到的请求向客户端发送响应信息。
- 简单快速:客户端向服务器请求服务时只需传送请求方法和路径,请求方法常用的有get、head、post。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,HTTP服务器程序规模小,因而通信快。
- 灵活:HTTP允许传输任意类型的数据对象,正在传输的类型由Content-Type加以标记。
- 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。从HTTP1.1起默认使用长连接,即服务器需要等待一定时间后才断开连接,以保证连接特性。目前的技术如keep-alive使用长连接优化效率,但这些属于HTTP请求之外。在每个独立的HTTP请求中,无法知道当前的HTTP是否处于长连接状态,始终认为HTTP请求在结束后连接就会关闭,这是HTTP的特性。下层实现是否在结束请求后关闭连接都不会改变这个特性。长连接可以理解为下层实现对上层透明。
- 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
HTTP协议目前处于多个版本共存的情况,包括仍被广泛采用的1.0、主流最为广泛的1.1、应用较少吹的最大的2.0。1.1较1.0引入了Keep-Alive长连接技术。2.0更合理更先进但推广不开的原因是1.1完全能够满足目前的应用,并且升级上2.0成本太大。以下以1.1为准。
Http1.0 1.1 2.0的区别?
参考:HTTP1.0 HTTP1.1 HTTP2.0 主要特性对比、HTTP1.0、HTTP1.1 和 HTTP2.0 的区别、HTTP/2 相比 1.0 有哪些重大改进?
客户端请求消息(Request)
客户端发送一个HTTP请求到服务器的请求消息结构如下:
- 请求行:请求方法post、get、put等等;协议版本HTTP1.0、1.1
- 请求头部:由若干个报头组成,每个报头都是名字+分号+空格+值,名字是大小写无关的。报头用来设置HTTP请求的参数,例如Host表示被请求资源的主机端口号,还有常用的头部字段Content-type
- 空格:数据体和头部之间有空行。请求头部后的空行是必须的,即使第四部分的请求数据为空。浏览器发送空行通知服务器结束了头信息的发送。
- 请求数据:请求正文、数据体。只在post请求中用到,表示要上传的数据。
服务端响应消息(Response)
HTTP响应结构:
服务器接收并处理客户端发来的请求后会返回HTTP的响应消息,即响应报文。
- 状态行:协议版本、状态码、状态码描述
- 响应头部:说明客户端附加信息,例如Content-type指定了MIME类型的HTML
- 响应的正文
HTTP协议定义了web客户端如何从web服务器请求web页面,以及服务器如何把web页面返回给客户端。HTTP协议采用请求响应模型,客户端向服务器发送一个请求报文,请求报文包含请求方法、URL、协议版本、请求头部和请求数据。服务器以一个状态行作为响应,响应的内容包含协议的版本、成功或错误的代码、服务器信息、响应头部和响应数据。
请求响应的步骤
- 客户端连接到web服务器:HTTP客户端通常是浏览器,与web服务器的HTTP端口(默认端口号80)建立TCP套接字连接。
- 发送HTTP请求:通过TCP套接字客户端向web服务器发送文本请求报文。
- 服务器接受请求并返回HTTP响应:web服务器解析该请求定位请求资源,服务器将资源副本写入TCP套接字,由客户端读取。
- 释放TCP连接,若连接模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接。若连接模式为Keep-Alive,则该连接会保持一段时间,在该时间内可以继续接收请求。
- 客户端浏览器解析HTML内容:客户端浏览器首先解析状态行,查看表明请求是否成功的状态码,解析每个响应头,响应头告知以下为若干字节的HTML文档和文档的字符集。客户端浏览器读取响应数据HTML根据HTML语法对其进行格式化,并在浏览器窗口中显示。
在浏览器地址栏键入URL,按下回车之后经历的流程
忽略掉诸如键盘事件响应之类的,只关心跟网络相关的知识。考虑最简单的流程。
- DNS解析:浏览器会依据URL逐层查询DNS服务器缓存,解析URL中的域名对应的IP地址,DNS缓存从近到远依次是浏览器缓存、系统缓存、路由器缓存、IPS服务器缓存、域名服务器缓存、顶级域名服务器缓存。从哪个缓存找到对应的IP直接返回,不再查询后面的缓存。
- TCP连接:结合三次握手
- 发送HTTP请求:浏览器发出读取文件的HTTP请求,该请求发送给服务器
- 服务器处理请求并返回HTTP报文:服务器对浏览器请求做出响应,把对应的带有HTML文本的HTTP响应报文发送给浏览器
- 浏览器解析渲染页面
- 连接结束:浏览器释放TCP连接,该步骤即四次挥手。第5步和第6步可以认为是同时发生的,哪一步在前没有特别的要求。
DNS域名解析流程
参考:面试宝典-DNS解析流程、DNS域名解析过程
常见的HTTP状态码
状态码由3位数字组成,第一位定义响应的类别
- 1XX:指示信息,表示请求以接收,继续处理
- 2XX:成功,表示请求已经被成功接收、理解、接受
200 OK:正常返回信息
- 3XX:重定向,要完成请求必须进行进一步操作
301:永久性转移,旧地址的资源已经被永久地移除了。搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址
302:暂时性转移,旧地址的资源还在,只是临时地从旧地址跳转到新地址,搜索引擎会抓取新的内容而保存旧的网址。
- 4XX:客户端错误,请求由语法错误或请求无法实现
400 Bad Request:客户端请求有语法错误,不能被服务器所理解。此时需要分析客户端的代码,去看看请求为什么出现服务 器无法理解的错误。
401 Unauthorized:缺失或错误的认证,这个状态代码必须和WWW-Authenticate报头域一起使用。
403 Forbidden:客户端请求没有权限去访问要求的资源。
404 Not Found:请求资源不存在,检擦URL或路径配置。
- 5XX:服务器错误,服务器未能实现合法的请求
500 Internal Server Error:服务器发生不可预测的错误。检查服务器日志,看看里面的代码哪里错误,进而抛出异常。
502 Bad Gateway:服务器作为网关或者代理时,为了完成请求访问下一个服务器,但该服务器返回了非法的应答。
503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常。
504 Gateway Timeout:网关超时,由作为代理或网关的服务器使用,表示不能及时地从远程服务器获得应答。
GET请求和POST请求的区别
- Http报文层面:GET将请求信息放在URL,POST放在报文体中。(GET请求信息与URL之间用?隔开,请求信息的格式为键值对。POST想获得请求信息必须解析报文。GET中的请求信息放在URL中,长度有限制,浏览器会对URL的长度做出限制。POST请求对数据长度没有限制)
- 数据库层面:GET符合幂等性和安全性,POST不符合。(幂等性:对数据库的一次操作和多次操作获得的结果是一致的。安全性:对数据库的操作没有改变数据库中的数据。GET操作是查询操作,不会改变数据库中原有的数据,大致认为符合安全性和幂等性。POST请求既不幂等也不安全,POST请求会往数据库中提交数据,会改变数据库中的数据;POST请求每次获得的结果都有可能不一样,因为POST作用在上一级的URL,每一次请求都会添加一份新资源)
- 其他层面:GET可以被缓存,被存储,而POST不行。GET请求被保存在浏览器的浏览记录中,GET请求的URL能保存为浏览器书签;POST方式不具备上述功能。缓存也是GET请求被广泛应用的根本,减少服务器的负担。
Cookie和Session的区别
- Cookie:
- Cookie是客户端的解决方案,是由服务器发送给客户端的特殊信息,以文本的形式存放在客户端。客户端每次向服务器发送请求时,都会带上这些特殊的信息。具体些,当用户使用浏览器访问一个支持Cookie的网站时,用户会提供包括用户名在内的个人信息,并且提交至服务器。服务器在向客户端回传相应的超文本的同时,也会发回这些个人信息,这些信息存放在HTTP响应头。当浏览器接受到来自服务器的响应后,浏览器会将这些信息存在一个统一的位置。客户端再向服务器发送请求时,会把相应的Cookie再次发送至服务器中。这次Cookie信息存放在HTTP请求头里面。服务器接收到请求后,会解析存放于请求头的Cookie,得到客户端特有的信息,动态生成和客户端相对应的内容。
- Cookie的设置和发送过程分以下4步:客户端发送一个HTTP请求到服务器端,服务端发送一个HTTP响应到客户端(包含Set-Cookie头部),客户端在发送一个HTTP请求到服务端(包含Cookie头部),服务器端发送一个HTTP响应到客户端。
- Session:
服务器端的机制,服务器采用类似散列表的结构来保存信息。当程序需要为某个客户端请求创建一个Session的时候,服务器首先检查客户端的请求里是否包含Session的标识,称为Session id。如果包含Session id,则说明以前为此客户端创建过Session。服务器就按照这个Session id,把Session检索出来使用,检索不到会新建一个;如果客户端请求不包含Session id,则为此客户端创建一个Session,并请生成与此Seesion相关的Session id。Session id的值是一个既不会重复用不容易被找到规律的字符串。Session id在本次响应中回发给客户端进行保存。
Session的实现方式主要有两种:
- 使用Cookie来实现。服务器给每个Session分配一个唯一的JSession id,并通过Cookie发送给客户端。客户端发起新的请求的时候,将在Cookie头中携带的JSession id,服务器找到客户端对应的Session。
- 使用URL回写来实现。URL回写是指服务器在发送给浏览器页面的所有链接都携带JSession id的参数,客户端点击任何一个链接,都会把JSession id带回服务器,如果直接在浏览器输入服务端资源的URL来请求该资源,Session是匹配不到的。
Tomcat对Session的实现是一开始同时使用Cookie和URL回写机制,如果发现客户端支持Cookie,就继续使用Cookie,停止使用URL回写;如果发现Cookie被禁用,就一直使用URL回写。
Cookie和Session的区别:
- Cookie数据存放在客户的浏览器上,Session数据放在服务器上。
- Session相对Cookie更安全
- Session会在一定时间保存在服务器上,考虑到服务器性能的开销,应到使用Cookie
单点登录
单点登录(SSO)看这一篇就够了
7.HTTP和HTTPS的区别
- HTTPS超文本传输安全协议,以计算机网络安全通信为目的的传输协议。在HTTP协议下加入了SSL层,从而具有了保护交换数据隐私和完整性,提供对网上服务器身份认证的功能。简单来说就是安全版的HTTP。SSL安全套接层,为网络通信提供安全及数据完整性的一种安全协议。位于TCP和各应用层之间,是操作系统对外的API,SSL3.0以后更名为TLS,采用身份验证和数据加密保证网络通信的安全和数据的完整性。
加密的方式(了解):
- 对称加密:加密和解密都使用同一个密钥。
- 非对称加密:加密使用的密钥和解密使用的密钥是不相同的,分为称为公钥和私钥。公钥算法公开,私钥保密。非对称加密算法性能较低,安全性强,能加密的数据长度有限。区块链很多都用到非对称加密。
- 哈希算法:将任意长度的信息转换为固定长度的值,算法不可逆。如MD5算法。
- 数字签名:证明某个信息或者文件是某人发出认同的。签名即在信息的后面加上一段内容,内容经过HashCode值可以证明信息没有被修改过。
- HTTPS采用证书配合各种加密手段的方式。
HTTPS数据传输流程:
- 浏览器将支持的加密算法信息发送给服务器。
- 服务器选择一套浏览器支持的加密算法,将验证身份的信息以证书的形式回发浏览器。证书信息包含证书发布的CA机构、证书的有效期、公钥、证书所有者、签名等等。CA机构是具备证书颁发资格的机构。
- 浏览器收到证书后首先验证证书的合法性,如果证书受到浏览器信任则在浏览器地址栏会有标志显示,否则就会显示不授信的标识。证书授信后web浏览器会随机生成一串密码,并使用证书中的公钥加密,之后使用约定好的Hash算法握手消息并生成随机数对消息进行加密,在将之前生成的消息回发给服务器。
- 当服务器接收到浏览器发送过来的数据后,会使用私钥将信息解密确定密码,通过密码解密web浏览器发送过来的握手信息并验证hash是否与web浏览器一致,服务器会使用密码加密新的握手信息发送给浏览器。
- 客户端浏览器解密并计算经过Hash算法加密的消息,如果与服务器发送过来的hash值一致,则此握手过程结束后服务器和浏览器会使用之前浏览器生成的随机密码和对称加密算法进行加密然后交换数据。
HTTP和HTTPS区别:
- HTTPS需要到CA申请证书(一般免费证书较少,需要一定的费用);HTTP不需要
- HTTPS是具有安全性的SSL加密传输协议,密文传输;HTTP信息是明文传输的
- 链接方式不同。HTTPS默认使用443端口;HTTP使用80端口
- HTTPS协议是由SSL+HTTP协议构建的可进行加密传输身份认证的网络协议,SSL是有状态的。HTTPS=HTTP+加密+认证+完整性保护,较HTTP安全;HTTP连接是无状态的
用户习惯访问网站时只输入一个域名,不会在域名前填充HTTP或HTTPS,浏览器默认填充http://,需要跳转到HTTPS。这个过程会使用到HTTP,有被劫持的风险,受到第三方的攻击。可以使用HSTS优化,HSTS目前正在推行中,没有成为主流,自己了解。
8.Socket相关
两个进程需要通信,需要唯一标识进程,在本地进程通信中,可以使用pid来唯一标识一个进程。但pid只在本地唯一,网络中的两个进程pid有可能冲突。
IP层的ip地址可以唯一标识一台主机,tcp协议和端口号可以唯一标识主机中的一个进程,IP地址+协议+端口号唯一标识网络中的一个进程。能够唯一标识网络中的进程后,他们就可以利用Socket进行通信。
Socket跟TCP/IP协议没有必然的联系,使程序员更方便的使用TCP/IP协议栈。Socket是对TCP/IP的抽象,从而形成了最基本的函数接口,如create、listen、connect、accept、send、read、write。Socket起源于UNIX,UINX准从一切皆文件的哲学,Socket是基于一种从打开到读和写再到关闭的模式去实现的。服务器和客户端各自维护一个文件,在建立连接打开后,可以向自己文件写入内容供对方读写或读写对方文件。
使用TCP协议通信的Socket为例,通信流程描述如下:
服务器先创建Socket,为Socket绑定IP地址和端口号,服务器的Socket会监听端口号的请求,随时准备接收客户端发来的连接,此时服务器的Socket只是Listen并没有打开。假设客户端创建了Socket,打开了Socket并根据服务器的IP地址和端口号尝试去连接服务器的 Socket。服务器的Socket接收到客户端的Socket请求被动的打开,开始接收客户端的请求直到客户端返回连接信息。此时服务器的Socket进入阻塞状态,阻塞即Accept方法需要一直等待客户端返回连接信息后才返回,同时开始接收下一个客户端的连接请求。客户端在连接成功后就会向服务器发送连接状态信息。服务器端在接收到客户端的连接信息之后就会将Accept方法返回并提示连接成功。之后客户端就可以向Socket写入信息,服务器就能收到并且读取相关的信息。最后在发送完数据后客户端就会关闭Socket,紧接着服务器也需要关闭Socket。
二. 数据库
1. 数据库架构
如何设计一个关系型数据库?
- 数据库最主要的功能是存储数据,因此有一个存储模块存储数据。存储模块类似OS文件系统,将数据持久化存入磁盘中,如存入机械硬盘、SSD固态硬盘、亦或者是它们的磁盘阵列矩阵中。
- 但是只有存储是不行的,还需要组织并且用到这些数据,因此需要有程序的实例,用逻辑结构来映射出物理结构,并且在程序中提供获取和管理数据的方式,以及提供必要的问题追踪机制。
- 细分程序模块:
- 数据逻辑关系转换成物理存储关系的存储管理模块:首先对数据的格式和文件的分隔进行统一的管理,即把物理数据通过逻辑的形式组织表示出来,便涉及到程序的存储管理模块。(优化存储效能:处理数据不在磁盘上做,而是加载到程序空间所在内存里,磁盘IO速率是程序执行速率的主要瓶颈,远差于内存的执行效率。为了执行效率,要尽可能减少IO。就存储管理而言,如果按照逐行查找并返回,频繁的IO会使数据库的执行效率慢。因为一次IO读取单条数据和多条数据没有太大的区别,所以可以一次性的读取多行,以提升IO的效能。行就失去了意义,数据以块和页作为逻辑存储单位,每个块和页中存放多行数据,读取的时候将多个块和页加载进内存中。)
- 优化执行效率的缓存模块:为了更快更好的优化利用内存,可以利用缓存机制,把取出来的数据块放进缓存里,下次需要的时候直接从内存返回,而不用发生IO。一次性加载多个模块或者页,块里包含的数据行有数据可能不是我们本次查询需要的行,但是一旦某行数据被访问了,它周围的数据也极有可能被访问的经验,缓存的非本质数据也能起到优化访问效率的作用,提升访问的性能。管理缓存的方法有LRU等。
- 将SQL语句解析的SQL解析模块:提供外部指令操纵数据,即可读的SQL语言,需要SQL解析模块将SQL编译解析,转换成机器可识别的指令。这时为了进一步提升SQL的执行效率,将SQL缓存到缓存里直接解析。缓存不宜过大,且有算法里淘汰机制,淘汰掉之后不常用的数据。
- 记录操作的日志管理模块:SQL操作需要记录下来,方便数据库的主从同步或者灾难恢复,因此需要日志管理对操作进行记录,如binlog的记录方式。
- 多用户管理的权限划分模块:还需要提供给用户管理数据的私密空间,即权限划分。通俗将就是老板可以看到员工的数据,员工只能看到自己该看到的数据。权限划分是DBA做的。
- 灾难恢复模块:除了考虑正常情况,还需考虑异常情况,需要引入异常机制,即容灾机制。当数据库挂了如何恢复,恢复到什么程度。
- 优化数据查询效率的索引模块和使得数据库支持并发操作的锁模块:为了进一步提升查询数据的速度以及让数据库支持并发,需要引入索引和锁模块。
2. 优化索引
为什么要使用索引?
快速查询数据
(最简单的方式实现数据查询,即全表扫描,将整张表的数据全部或者分批次加载到内存中。存储的最小单位是块或者页,他们是由多行数据组成。将块加载进来,逐个块轮询,找到目标并返回。这种方式普遍比较慢。很多情况下都要避免全表扫描情况的发生,所以数据库引入更高效的机制,即索引。关键信息和查找信息的方式组成索引,通过索引可以大幅提升查询速度。)
什么样的信息能成为索引?
主键、唯一键、普通键
(把记录限定在一定查找范围内的字段,主键便是一个很好的切入点,其他包括唯一键、普通键等也可以作为索引。)
索引的分类?
主键索引(列值唯一,表中只有一个)、
唯一索引(列值唯一)、
普通索引、
全文索引、
联合索引
创建索引
直接创建索引
-- 创建唯一索引
CREATE UNIQUE INDEX index_name ON table_name(col_name);
-- 创建普通索引
CREATE INDEX index_name ON table_name(col_name);
-- 创建唯一组合索引
CREATE UNIQUE INDEX index_name ON table_name(col_name_1,col_name_2);
-- 创建普通组合索引
CREATE INDEX index_name ON table_name(col_name_1,col_name_2);
通过修改表结构创建索引
ALTER TABLE table_name ADD INDEX index_name(col_name);
创建表的时候直接指定
CREATE TABLE mytable(
id INT NOT NULL,
username VARCHAR(16) NOT NULL,
INDEX [indexName] (username(length))
);
删除索引
-- 直接删除索引
DROP INDEX index_name ON table_name;
-- 修改表结构删除索引
ALTER TABLE table_name DROP INDEX index_name;
查看索引
#查看:
show index from `表名`;
#或
show keys from `表名`;
其他命令
-- 查看表结构
desc table_name;
-- 查看生成表的SQL
show create table table_name;
索引的数据结构?
生成索引,建立二叉查找树进行二分查找。
生成索引,建立B-Tree结构进行查找。
生成索引,建立B±Tree结构进行查找。
生成索引,建立Hash结构进行查找。
(让查询变得高效的数据结构,如二叉查找树和二叉查找树的变种平衡二叉树、红黑树、BTree、B+Tree以及Hash结构。MySQL数据库索引是通过B+Tree实现。)
2.1 二叉树
- 二叉查找树是每个节点最多有两个子树的树结构,通常子树被称为左子树或右子树。左子树节点的值均小于根节点,右子树节点的值均大于根节点(注意索引的存储块和数据库的最小存储单位块或者页并非一一对应,为了方便理解先一一对应起来)。每个存储块存储的是关键字和指向子树的指针。平衡二叉树任意一个节点的左子树和右子树高度差不超过1。
- 查询时间复杂度O(logn),查询效率高。极端情况(节点全部在左子树或右子树上)时间复杂度将为O(n)。可以利用树的旋转的特性保持树为平衡二叉树。但还有另一个问题,影响程序运行速度的瓶颈是IO。如果假定索引块在磁盘中,找索引会先发生一次IO,将数据读入内存中,之后再发生IO继续查找,直到找到。检索深度每增加1,就发生一次IO。平衡二叉树、红黑树等每个节点只能有两个孩子。为了组织起数据块,树的深度很深,IO的次数也会很多,检索性能没法满足优化查询需求。
- 即降低查询的时间复杂度,又降低IO的次数,要让树每个节点能承受的数据多一些,即利用B-Tree、B±Tree。
2.2 B树
B树,即平衡多路查找树。每个节点最多有m个孩子,这样的树即为m阶B树。每个存储块主要包含关键字和指向孩子的指针,最多能有几个孩子取决于每个存储块的容量和数据库的相关配置(通常情况下m是很大的)。
B树特征:
- 根节点至少包括两个孩子。
- 树中每个节点最多含有m个孩子(m>=2)。
- 除根节点和叶节点外,每个节点至少有ceil(m/2)个孩子。ceil向上取整
- 所有叶子节点位于同一层。
- 假设每个非终端节点中包含有n个关键字信息,其中:
- Ki(i=1…n)为关键字,且关键字按顺序升序排序K(i-1)<K(i)。
- 关键字的个数n必须满足:[ceil(m/2)-1]<=n<=m-1。(任意节点的关键字个数上限比它的孩子数上限少一个,且对于非叶子节点来说,任何一个节点的关键字个数比指向孩子的指针数少一个)
- 非叶子节点的指针:P[1],P[2]…P[M],其中P[1]指向关键字小于K[1]的子树(某节点最左边孩子节点关键字的值均小于该节点最左边关键字的值),P[M]指向关键字大于K[M-1]的子树(某节点最右边孩子节点的关键字的值均大于该节点里所有关键字的值),其他P[i]指向关键字属于(K[i-1],K[i])的子树(某节点其余孩子节点关键字的值的大小均位于离该孩子节点指针最近的两个关键值之间)。
查找效率和二叉查找树一样,为O(logn)。B树通过合并、分裂、上移、下移节点保持特征,使树比二叉树矮,数据不断变动后不会变成线性的。
B树示例:
2.3 B+树
B+树是B树的变体,其定义基本与B树相同,除了:
- 非叶子节点的子树指针与关键字个数相同。(B+树能存储更多的关键字)
- 非叶子节点的子树指针P[i],指向关键值(K[i],K[i+1])的子树。(K[i]指向的子树,均小于关键字K[i+1]的值)
- 非叶子节点仅用来索引,数据都保存在叶子节点中。(B+树所有的检索都是从根部开始,检索到叶子节点结束,非叶子节点仅存储索引不存储数据,能存储更多的数据。B+树相对B树更矮。B树的搜索可能在任何一个非叶子节点就终结掉了。)
- 所有叶子节点均有一个链指针指向下一个叶子节点并按大小顺序链接。(支持范围统计,即定位到某个叶子节点便可以从该叶子节点开始横向跨子树统计。)
B+树示例:
B+树更适合用来做存储索引:
- B+树的磁盘读取代价更低(B+树内部结构没有指向关键字具体信息的指针,不存放数据,只存放索引信息。内部节点相对B树更小。如果把所有内部节点的关键字存放在同一盘块中,盘块能容纳的关键字数量也越多,一次性读入内存查找的关键字也就越多,相对来说IO读写次数低)。
- B+树的查询效率更加稳定(内部节点不是指向文件内容的节点,只是叶子节点中关键字的索引,任何节点的查找必须有一条从叶子节点到根节点的路,所有关键字查询的长度相同,每个数据的查询时间相同,O(logn))。
- B+树更有利于对数据库的扫描(B+树只需要遍历叶子节点就可以解决对全部数据的扫描)。
hash以及BitMap
Hash索引
根据Hash函数的运算只需1次定位便能找到需要查询数据所在的头。Hash索引的查询效率理论上高于B+树索引。
缺点:
- 仅仅能满足“=”,“IN”,不能使用查询范围。(Hash索引比较的是进行Hash运算后的Hash值,只能用于等值的过滤,不能用于基于范围的查询,因为经过相应的Hash算法处理过的Hash值的大小关系不能保证和Hash运算前的完全一样。)
- 无法被用来避免数据的排序操作。
- 不能利用部分索引键查询。(对于组合索引,Hash索引在计算Hash值的时候是组合键,将键组合合并后在一起计算Hash值,而不是单独计算Hash值。通过组合索引的前一个或几个索引键进行查询时Hash索引也无法被利用。B+树支持利用组合索引中的部分索引。)
- 不能避免表扫描。(Hash索引是将索引键通过Hash运算后将运算结果的Hash值和所对应的行指针存放在一个Backet中,不同的索引键具有相同的Hash值,所以取出满足某个Hash键值的数据也无法从Hash索引中直接完成查询,还是需要访问Backet中的数据进行比较。)
- 遇到大量Hash值相等的情况性能并不一定会比B树索引高。
BitMap位图索引
当表中的某个字段只有几种值的时候,在该字段上实现高效统计用位图索引是最佳的选择。目前很少数据库支持位图索引,已知比较主流的是Oracle。位图索引的结构类似B+树。在存储方式上会先按照状态值分开,每种值的空间存放每个实际的数据行是否是这个值。因为只需要存放是与否,所以只需要一个Bit位存放。理论上一个叶子块可以存放非常多的Bit位来表示不同的行。
缺点:
锁的密度非常大,当尝试新增或修改数据时,与它在同一个位图的数据操作都会被锁住。因为某行所在的位置顺序会因为数据的添加或者删除而发生改变。不适合高并发的联机事务处理系统,即常见的OLTP系统。而适合并发较少,统计数据较多的OLAP系统。
3. 密集索引和稀疏索引区别
密集索引文件中的每个搜索码值都对应一个索引值。(叶子节点不仅保存键值,还保存了位于同一行记录里的其他列的信息。密集索引决定了表的物理排列顺序,一个表只能有一个物理排列顺序,所以一个表只能创建一个密集索引。)
稀疏索引文件只为索引码的某些值建立索引项。(叶子节点仅保存键位信息和该行数据的地址,有的稀疏索引仅保存键位信息及其主键。定位到叶子节点仍需要地址或主键信息进一步定位到数据。)
MySQL常见的两种的存储引擎:
MyISAM:主键索引、唯一键索引、普通索引其索引均属于稀疏索引
InnoDB:必须有且仅有一个密集索引,密集索引的选取规则如下:
- 若一个主键被定义,则该主键作为密集索引。
- 如果没有主键被定义,该表的第一个唯一非空索引则作为密集索引。
- 若不满足以上条件,innodb内部会生成一个隐藏主键(密集索引)。
- 非主键索引存储相关键位和其对应的主键值,包含两次查找。(非主键索引即稀疏索引的叶子节点不存储行数据的物理地址,而是存储的该行的主键值,所以非主键索引包含两次查找,一次查找次级索引自身,再查找主键。见下图左)
InnoDB使用密集索引,将主键组织到一棵B+树中,行数据就存储在叶子节点上。因为InnoDB的主键索引和对应的数据是保存在同一个文件,检索时在加载叶子节点的数据进入内存时,也加载了对应的数据。若对稀疏索引进行条件筛选,首先在稀疏索引的B+树中检索该键,获取到主键信息。然后利用主键在密集索引B+树中再执行一遍检索操作,最终到达叶子节点,获取整行数据。
MyISAM均为稀疏索引,稀疏索引的两棵B+树节点结构完全一致,只是存储的内容不一样。主键索引B+树存储主键,辅助键索引B+树存储辅助键,表数据存储在独立的地方,索引和数据是分开存储的。两棵B+树的叶子节点都使用地址指向真正的表数据。对于表数据来说,两个键没有任何差别。通过辅助键检索无需访问主键的索引树。
参考:聚簇索引和非聚簇索引、如何避免回表查询?什么是覆盖索引?
4. 索引额外问题
4.1 调优SQL
如何定位并优化慢查询SQL?
- 根据日志定位慢查询sql
- 慢日志是记录执行的比较慢的SQL。
执行show variable like ‘%query%’;显示long_query_time为10秒即SQL执行时间超过10秒会被记录在慢日志中,slow_query_log为OFF慢日志为关闭状态,slow_query_log_file慢日志存储地址。通过set global slow_query_log=on;设置打开慢日志。该语句只是暂时保存,重启数据库服务会还原成原来的样子
- show status like ‘%slow_queries%’;显示慢查询的条数。
通过慢日志捕获慢sql,进而分析sql为什么慢,然后对它进行调优。
- 使用explain等工具分析SQL
- 关键字放在select查询语句的前面,用于描述MySQL如何执行查询操作,以及MySQL成功返回结果集需要执行的行数。explain可以分析select语句,知道查询效率低下的原因,从而改进查询。
- explain关键字字段
- type
MySQL找到需要数据行的方式,性能从最优到最差排序如下:
system,const,eq_ref,ref,fulltext,ref_or_null,index_merge,unique_subquery,index_subquery,range,index,all
index和all查询是全表扫描。
- extra
extra中出现以下两项,意味着MySQL根本不能使用索引,效率会受到重大影响,应尽可能对此进行优化
1.Using filesort :表示MySQL会对结果使用一个外部索引排序,而不是从表里按索引次序读到相关内容。可能在内存或者磁盘上进行排序。MySQL中无法利用索引完成的排序操作称为“文件排序”
2.Using temporary: 表示MySQL在对查询结果排序时使用临时表。常见于排序order by和分组查询group by
- 修改SQL或尽快让SQL走索引
只有DML数据操纵语言才会进慢查询语句中,DDL数据定义语言不会进入慢SQL 。
DQL 数据查询语言 select
DML 数据操纵语言 insert、update、delete
DDL 数据定义语言 crete、drop
DCL 数据控制语言 grant、revoke
4.2 最左匹配原则的成因
联合索引:由多列组成的索引
最左匹配原则:假设有两列A、B,对A设置联合索引,即将A和B都设置为索引,顺序是A、B。在where语句中调用where A=? and B=?,会走这个索引; 调用where A=?也会走这个索引;调用where B=?就不走这个索引了。
- 最左匹配非常重要的原则,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=1 and b=2 and c=3建立(a,b,c)索引可以任意顺序,MySQL的查询优化器会帮你优化成索引可以识别的形式。
MySQL创建联合索引首先会对复合索引最左边即第一个索引字段的数据进行排序,在第一个排序字段的基础上再对后面第二个索引字段进行排序,类似实现了order by 字段1 order by 字段2,第一个字段绝对有序第二个字段无序。因此MySQL用第二个字段进行条件判断是用不到索引的。
4.3 索引是建立越多越好吗
不是。
- 数据量小的表不需要建立索引,建立会增加额外的索引开销。
- 数据变更需要维护索引,因此更多的索引意味着更多的维护成本。
- 更多的索引意味着需要更多的空间。
索引失效
参考: 索引失效
5. MyISAM与InooDB区别
MyISAM默认使用表级锁,不支持行级锁;
InnoDB默认用的行级锁,也支持表级锁。
无论是表锁还是行锁,均分为共享锁share lock(S)和排它锁exclusive lock(X)。
MyISAM
- MyISAM先上读锁后上写锁(被Block)、读锁(不被Block)
- MyISAM对数据进行select时,自动加上一个表级读锁,表级锁自动锁住整张表;对数据进行增删改时,操作表加上一个表级别的写锁。读锁未被释放时,另外一个Session(数据库客户端一个窗口tab就是一个Session)想要对该表加上一个写锁就会被阻塞(Block),直到所有的读锁都被释放为止。
- 显示给表加上读锁:lock table 表名 read;
释放锁:unlock tables;- 读锁也叫共享锁(S锁),因为在进行范围查询时依然能对表里的数据进行读操作。
- MyISAM先上写锁后上读锁(被Block)、写锁(被Block)
当上了写锁在上读锁时,需要等待写锁的释放。上写锁的同时再上写锁,也被阻塞。所以写锁也叫排它锁(X锁)。
上共享锁后依然支持上共享锁,上排它锁后共享锁和排它锁都不支持。- 除了可以对insert、update、delete语句上排它锁,也可以对select语句上排它锁。在语句后面加上for update。
InnoDB
- InnoDB用的二段锁,即加锁和解锁是分成两个步骤。先对同一个事务里的一批操作进行加锁,commit后再对事务加上的锁进行统一的解锁。MySQL自动提交事务,即commit是自动提交的。
- InnoDB在SQL没用到索引时走的是表级锁,用到索引时走的时行级锁和gap锁。
- InnoDB的锁默认支持行级锁。InnoDB对select进行了改进,在select语句后面加lock in share mode显示上读锁,才不可以上写锁。可以上共享锁。
- InnoDB除了支持行级锁外,还支持表级意向锁,意向锁分为意向共享锁IS、意向排它锁IX,作用是在进行表级别操作时不用轮询每一行看有没有上行锁。
数据库锁的分类
- 按锁的粒度划分,可以分为表级锁、行级锁、页级锁。(BDB引擎使用页级锁,介于表级锁和行级锁,锁定位于同一个存储页的相邻几行数据)
- 按锁级别划分,可分为共享锁和排它锁。
- 按加锁方式划分,可分为自动锁、显式锁。
- 按操作划分,可分为DML锁、DDL锁。
- 按使用方式划分,可分为乐观锁和悲观锁。
悲观锁对外界的修改持保守态度,外界指即本系统当前的其他事务和外部系统的事务处理。全程用排它锁锁定是悲观锁的一种实现。悲观并发控制是先取锁再访问的保守策略,对数据处理的安全提供了保证。在效率方面处理加锁的机制会产生额外的开销,增加产生死锁的机会;
乐观锁认为数据一般情况不会造成冲突,数据提交更新时才会对数据的冲突与否进行检测,发现冲突返回用户错误的信息,让用户决定如何去做。
相对悲观锁对数据进行处理时,乐观锁不会使用事务的锁机制,一般实现乐观锁的方式是记录数据版本。实现数据版本有两种方式:第一种是使用版本号;第二种是使用时间戳)
6. 数据库事务的四大特性
事务是访问并可能更新数据库中各种数据项的一个程序执行单元。
ACID
- A原子性:事务包含的所有操作要么全部执行,要么全部失败回滚。要么全做,要么全不做。
- C一致性:事务应确保数据库的状态从一个一致状态转变为另外一个一致的状态。以转账为例,A账户+B账户=2000,无论A和B如何转账,转几次账,A和B的钱加起来还是2000。
- I隔离性:多个事务并发执行时一个事务的执行不影响其他事务的执行。下面的知识点是对隔离性的深入研究。
- D持久性:一个事务一旦提交,对数据库的修改永久保存在数据库中。当系统或者介质发生故障时确保已提交事务的更新不能丢失,即对已提交事务的更新能恢复。一旦一个事务被提交,DBMS保证提供适当冗余,使其耐得住系统的故障。
7. 事务并发访问产生的问题及事务隔离机制
事务并发访问引起的问题以及如何避免?
- 更新丢失–MySQL所有事务隔离级别在数据库层面上均可避免
- 脏读(一个事务读到另一个事务未提交的数据)–Read-Committed即RC事务隔离级别以上可以避免(Read-Committed规定事务只能读取其他事务已经提交的数据,不允许读未提交的数据)
查询当前Session的事务隔离级别:
select @@tx_isolation;
设置当前Session的事务隔离级别为read uncommitted:
set session transaction isolation level read uncommitted;
- 不可重复读(事务A多次读取同一数据,事务B在事务A读取数据时对数据更新并提交,导致事务A多次读取数据时数据不一致)–Repeatable-Read即RR事务隔离级别以上可以避免
- 幻读(事务A读取与搜索条件相匹配的若干行,事务B以插入或删除行的方式来修改事务A的结果集,导致事务A看起来像出现幻觉一样)–Serializable事务隔离级别可避免
不可重复读侧重于对同一数据的修改,幻读侧重于新增或删除。
事务隔离级别越高,安全性越高,串行化执行越严重,降低数据的并发度。
根据业务的需要设置事务的隔离级别。
Oracle默认为Read-Committed,
MySQL默认为Repeatable-Read。
8. 当前读和快照读(InnoDB可重复读隔离级别下如何避免幻读?)
- 表象:在RR级别下,基于伪MVCC(多版本并发控制,读不加锁,读写不冲突)实现的快照读(非阻塞读)来避免使我们看到幻行
- 内在:next-key锁(行锁+gap锁)
当前读:select…lock in share mode、select…for update、update、delete、insert。加了锁的增删改查语句,不管是共享锁还是排它锁。读取的是记录的最新版本,读取之后还需要保证其他并发事务不能修改当前记录,对读取的记录加锁,所以叫当前读。除了select…lock in share mode对记录加共享锁,其他都加排它锁。
为什么update、delete、insert也是当前读? RDMS关系型数据库管理系统由两部分组成,程序实例和存储InnoDB,如图。update操作内部包含一个当前读来获取数据的最新版本。
快照读:不加锁的非阻塞读,select。不加锁是在事务隔离级别不为Serializable的前提下。在Serializable下,由于是串行读,快照读退化成当前读,即select…lock in share mode。快照读是为了提升并发性能,快照读的实现是基于多版本并发控制即MVCC,MVCC是行级锁的一个变种,在很多情况下避免了加锁操作,因此开销更低。基于多版本意味着快照读读到的不一定是数据的最新版本,可能是历史版本。
9. RR如何避免幻读(RC,RR级别下的InnoDB的非阻塞读如何实现?MVCC)
- 数据行里DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID字段
- 每行数据的记录除了存储数据外,还有额外的字段,其中最重要的就是DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID。DB_TRX_ID字段标识最近一次对本行记录做修改,不管是insert或update,事务的标识符,即最后一次修改本行记录的事务的ID。
- DB_ROLL_PTR回滚指针,写入回滚段Rollback segment的undo日志记录,如果一行记录被更新,undo log report包含重建该行记录被更新之前内容所必须的信息。
- DB_ROW_ID行号,包含一个随着新行插入而单调递增的行id,由innoDB自动产生聚集索引时,聚集索引会包含行id的值,否则行id不会出现在任何索引中。InnoDB的表即没有主键也没有唯一键时,InnoDB会自动隐式创建一个的自动递增隐藏主键字段,即DB_ROW_ID。
- 光有这三个字段不足以实现快照读,还需要undo日志。当对记录做了变更操作时,就会成undo记录。undo记录存储的是老版数据,当一个旧事务需要读取数据时,为了读取老版数据,需要顺着undo链找到满足其可见性的记录。undo log主要分为两种,insert undo log和update undo log,insert undo log表示事务对insert新纪录产生的undo log,只在事务回滚时需要,事务提交后就可以立即丢弃。update undo log是事务在对数据delete、update时产生的undo log,不仅在事务回滚时需要,快照读也需要,所以不能随便删除。只有当数据库使用的快照不涉及该日志记录,对应的回滚日志才会被线程删除。
日志的工作方式:
事务对行记录的更新过程。InnoDB在内部做了非常多的工作。假设对DB_ROLE_ID为1的行做变动,被事务A做修改,将Field2的值由12改为32,修改流程如下:首先用排他锁锁定该行,将该行修改前的值拷贝一份到undo log里面,修改当前行的值,填写事务ID即DB_TRX_ID,使用回滚指针指向undo log中修改前的行。之后假设数据库还有别的事务使用快照读读取该日志记录,此时某个事务又对同一行做了修改,Field3由13改为45,效果和刚刚一样,又多了一条undo记录。按照修改的时间顺序由近到远,通过DB_ROLL_PTR连接起来。
- read view
- read view做可见性判断,当进行快照读Select时对针对查询出的数据做read review来决定当前事务能看到的是哪个版本的数据,有可能是最新版本的数据,也有可能是undo log某个版的数据。read review遵循一个可见性算法,将要修改的数据的DB_TRX_ID与系统其他活跃事务ID做对比,大于等于这些ID,就通过DB_ROLL_PTR取出undo log上一层的DB_TRX_ID,直到小于这些活跃事务ID为止,这样保证当前数据版本是当前可见的最稳定版本。
- RR级别下,Session在start transaction后,第一条快照读会创建一个快照read view,将当前系统中活跃的其他事务记录,此后在调用快照读还是用同一个read;RC级别下,事务中每条Select语句即每次调用快照读时都会创建一个新的快照。所以在RC下能用快照读看到别的事务提交的对表事务的增删,在RR下首次使用快照读是在别的事务对数据做出增删改并提交之前的,此后即使别的事务对数据做了增删改并提交还是都不到数据变更。对RR首次事务提交的时机是重要的。
- gap锁:gap是指索引数中插入新记录的空隙,gap锁即间隙锁锁定一个范围但不包括记录本身,gap锁的目的是为了防止同一事务的两次当前读出现幻读的情况,gap锁在read commit和更低级别的事务隔离级别下是没有的,RR和Serializable默认支持gap锁。
- RR下gap锁出现的场景:对主键索引或者唯一键索引,如果where条件全部命中,则不会用gap锁,只会加记录锁;如果where条件部分命中或者全不命中,则会加gap锁。
- gap锁会用在非唯一索引或者不走索引的当前读中。
10. MyISAM和InnoDB引擎区别
- 存储结构(主索引/辅助索引)
InnoDB的数据文件本身就是主索引文件。而MyISAM的主索引和数据是分开的。
InnoDB的辅助索引data域存储相应记录主键的值而不是地址。而MyISAM的辅助索引和主索引没有多大区别。
InnoDB是聚簇索引,数据挂在主键索引之下。- 锁
MyISAM使用的是表锁;InnoDB使用行锁- 事务
MyISAM没有事务支持和MVCC;InnoDB支持事务和MVCC- 全文索引
MyISAM支持FULLTEXT类型的全文索引;InnoDB不支持FULLTEXT类型的全文索引,但是InnoDB可以使用sphinx插件支持全文索引,并且效果更好。- 主键
MyISAM允许没有任何索引和主键的表存在,索引都是保存行的地址;InnoDB如果没有设定主键或非空唯一索引,就会自动生成一个6字节的主键,数据是主索引的一部分,附加索引保存的是主索引的值。- 外键
MyISAM不支持;InnoDB支持
11. 关键语法
- group by:给定数据列的每个成员,对查询结果进行分组统计,最终得到一个分组汇总表。
对同一张表
- select子句的列名必须为分组列(group by用到的列)或列函数(count、sum、max、min、avg)
- 列函数对于group by子句定义的每个组各返回一个结果
- order by :根据指定的列对结果集进行排序,默认按照升序(ASC)对记录进行排序,降序使用 DESC 关键字。
- having:
- 通常与group by子句一起使用(在group by后指定过滤的条件,省略group by,having就和where一样)
- where过滤行,having过滤组
- 出现在同一sql的顺序:where>group by>having
- 统计相关(聚合函数):count求总数、sum求和、max求最大值、min求最小值、avg求平均。
- 在MySQL数据库中,聚合函数不能出现在where语句中,聚合函数的实现是基于所有数据的基础上,where语句是对数据进行筛选的。
- 查询前N条记录:limit
12. 数据库范式
第一范式:属性不可分割
第二范式:要求表中要有主键,表中其他其他字段都依赖于主键(主键约束)
第三范式:要求表中不能有其他表中存在的、存储相同信息的字段,不得存在传递依赖(外键约束)
13. 主从复制、读写分离、分库分表
binlog和redolog的区别
- redolog是在InnoDB存储引擎层产生,而binlog是MySQL数据库的上层服务层产生的。
- 两种日志记录的内容形式不同。MySQL的binlog是逻辑日志,其记录是对应的SQL语句,对应的事务。而innodb存储引擎层面的重做日志是物理日志,是关于每个页(Page)的更改的物理情况。
- 两种日志与记录写入磁盘的时间点不同,binlog日志只在事务提交完成后进行一次写入。而innodb存储引擎的重做日志在事务进行中不断地被写入,并日志不是随事务提交的顺序进行写入的。
- binlog不是循环使用,在写满或者重启之后,会生成新的binlog文件,redolog是循环使用。
- binlog可以作为恢复数据使用,主从复制搭建,redolog作为异常宕机或者介质故障后的数据恢复使用。
Mysql读写分离以及主从同步
- 原理:主库将变更写binlog日志,然后从库连接到主库后,从库有一个IO线程,将主库的binlog日志拷贝到自己本地,写入一个中继日志中,接着从库中有一个sql线程会从中继日志读取binlog,然后执行binlog日志中的内容,也就是在自己本地再执行一遍sql,这样就可以保证自己跟主库的数据一致。
- 问题:这里有很重要一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行操作,在从库上会串行化执行,由于从库从主库拷贝日志以及串行化执行sql特点,在高并发情况下,从库数据一定比主库慢一点,是有延时的,所以经常出现,刚写入主库的数据可能读不到了,要过几十毫秒,甚至几百毫秒才能读取到。还有一个问题,如果突然主库宕机了,然后恰巧数据还没有同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。所以mysql实际上有两个机制,一个是半同步复制,用来解决主库数据丢失问题,一个是并行复制,用来解决主从同步延时问题。
- 半同步复制:semi-sync复制,指的就是主库写入binlog日志后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的relay log之后,接着会返回一个ack给主库,主库接收到至少一个从库ack之后才会认为写完成。
- 并发复制:指的是从库开启多个线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志,这样库级别的并行。(将主库分库也可缓解延迟问题)
三. Redis
1. Redis简介
MySQL的数据都是存放在磁盘中的,虽然在数据库层也做了对应的缓存,但这种数据库层次的缓存一般针对查询的内容,而且粒度也比较小。一般只有表中数据没有发生变动时,数据库对应的Cache才会发挥作用,这不能减少业务系统对数据库产生的增删改查的IO压力。因此缓存数据库应运而生,该技术实现了对热点数据的高速缓存,提高应用的响应速度,极大缓解后端数据库的压力。
缓存中间件–Memcache和Redis的区别
- Memcache对数据类型的支持简单,只支持简单的key-value;不支持数据持久化存储(数据全部存在内存之中,一旦服务器宕机数据没办法保存);不支持主从;不支持分片。
- Redis数据类型丰富;有部分数据存在硬盘上,这样能保证数据的持久性;支持主从;支持分片。redis目前官方只支持LINUX 上去行,从而省去了对于其它系统的支持,这样的话可以更好的把精力用于本系统 环境上的优化。底层模型上,新版本的redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
为什么Redis这么快?
100000+QPS(QPS即query per second,每秒内查询次数)
- 完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高。Redis是采用单进程单线程模型的K-V数据库,由c语言编写,将数据存储到内存中,读写数据的时候都不会受到硬盘IO速度的限制。
- 数据结构简单,对数据操作也简单。Redis不使用表,它的数据库不会预定义或者强制要求用户对Redis存储的不同数据进行关联,因此性能相比关系型数据库要高出不止一个量级,其存储结构就是键值对,类似于hashMap。hashMap的优势就是查找和操作的时间复杂度都是O(1)的。
- 采用单线程,单线程也能处理高并发请求,想多核也可启动多实例。在面对高并发的请求的时候,首先想要的是多线程来进行处理,将IO线程和业务线程分开,业务线程使用线程池来避免频繁创建线程和销毁线程,即便是一次请求,阻塞了也不会影响到其它请求。Redis单线程结构是指主线程是单线程的,主线程包含IO事件的处理,以及IO对应的相关请求的业务处理。此外,主线程还负责过期键的处理、复制协调、集群协调等等。这些除了IO事件之外的逻辑会被封装成周期性的任务,由主线程周期性的处理。因为采用单线程的设计,对于客户端的所有读写请求,都由一个主线程串行的处理,因此多个客户端同时对一个键进行写操作的时候,就不会有并发的问题,避免了频繁的上下文切换和锁竞争,使得Redis执行起来效率更高。单线程是可以处理高并发的请求的,并发不是并行,并行性意味着服务器能够同时执行几个事情,具有多个计算单元,而并发性IO流意味着能够让一个计算单元来处理来自多个客户端的流请求。Redis使用单线程配合上IO多路复用,可以大幅度的提升性能。CPU不是制约redis的性能瓶颈,此外,可以在多核的服务器中启动多个Redis实例来利用多核的特性。
注意,这里的单线程只是在处理我们的网络请求的时候,只有一个单线程来处理,一个正式的Redis server,在运行的时候,肯定不止一个线程的。例如Redis在进行持久化的时候,会根据实际情况,以子进程或者子线程的方式执行。
- 使用多路I/O复用模型,非阻塞IO。Redis是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或者输出都是阻塞的,所以IO操作在一般情况下,往往不能直接返回,就会导致某一文件的IO阻塞,进而导致整个进程无法对其它客户端提供服务。而IO多路复用就是为了解决这个问题而出现的。
多路IO复用模型
- FD
File Descriptor,文件描述符。在操作系统中,一个打开的文件通过唯一的描述符进行引用,该描述符是打开文件的元数据到文件本身的映射,在Linux内核中,该描述符称为文件描述符即File Descriptor,文件描述符用一个整数来表示。- 传统的阻塞I/O模型
当使用read或者write对某一个文件描述符FD进行读写的时候,如果当前的FD不可读或者不可写,整个Redis服务就不会对其它的操作做出响应,导致整个服务不可用,这也就是传统意义上的阻塞模型,阻塞模型会影响其它FD对应的服务,所以在需要处理多个客户端任务的时候,往往都不会使用阻塞模型。此时,需要一种更高效的I/O模型来支持Redis的高并发处理,就是I/O多路复用模型。
I/O多路复用模型,最重要的函数调用就是Select系统调用。Select可以同时监控多个文件描述符的可读可写情况,当其中的某些文件描述符可读或者可写时,Select方法就会返回可读以及可写的文件描述符个数,也就是说,Selector是负责监听我们的文件是否可读或者可写的,监听的任务交给Selector之后,程序就可以做其它的事情,而不被阻塞。- 多路复用函数
- 与此同时,也有其它的I/O多路复用函数,Redis采用的I/O多路复用函数:epoll、kqueue、evport、select。epoll、kqueue、evport相比select的性能更加优秀的,同时也可以支撑更多的服务。
- Redis采用的多路复用函数是因地制宜的。Redis需要在多个平台下运行,为了最大化的提高执行效率和性能,会根据编译平台的不同选择不同的IO多路复用函数作为子模块,提供给上层统一的接口。
- Redis优先选择时间复杂度为O(1)的IO多路复用函数作为底层实现。
- 以时间复杂度为O(n)的select作为保底。如果没有epoll、kqueue、evport,就会使用select,select在使用时会扫描全部的文件描述符,性能较差,时间复杂度是O(n)。
- 基于react设计模式监听I/O事件。Redis服务采用react设计模式来实现文件处理器。文件事件处理器使用I/O多路复用模块,同时监听多个FD,当accept、read、write等文件事件产生的时候,文件事件处理器就会回调FD绑定的事件处理器,虽然整个文件事件处理器是在单线程运行的,但是通过I/O多路复用模块的引用,实现了同时对多个FD读写的监控,提高了网络通信模型的性能,同时来保证了整个Redis服务实现的简单。
2. Redis常用数据类型
常用数据类型
- String:最基本的数据类型,二进制安全,Redis的String可以包含任何数据,比如jpg图片或者序列化的图像。常用在缓存、计数、共享Session、限速等。
- Hash:hash类型是一个string类型的field和value的映射表,每个 hash 可以存储 2^32 - 1 键值对(40多亿),hash类型的结构(key, field, value),适合用于存储对象。哈希可以用来存放用户信息,比如实现购物车。
- List:列表,按照String元素插入顺序排序,是简单的字符串列表。类似栈,先进后出的顺序。可以做简单的消息队列的功能。
- Set:集合,String元素组成的无序集合,通过哈希表实现,不允许重复。添加、删除、查找的复杂度是O(1)。提供了并集、交集、叉集操作。
- Sorted Set:通过分数来为集合中的成员进行从小到大的排序。Redis的zset和set集合一样,也是String集合组成的集合,且不允许重复的成员,不同的是有序集合每个元素都会关联一个double类型的分数,redis正是通过这个分数,来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但是分数却可以重复。分数越小越靠前。可以做排行榜应用,取 TOP N 操作。
- 用于计数的HyperLoglog,用于支持存储地理位置信息的Geo等等。
底层实现
参考:Redis数据结构底层实现
3. Redis数据过期策略、缓存雪崩、缓存穿透、缓存击穿
数据过期策略
Redis 中数据过期策略采用定期删除+惰性删除策略
- 定期删除策略:Redis 启用一个定时器定时监视所有的 key,判断key是否过期,过期的话就删除。这种策略可以保证过期的 key 最终都会被删除,但是也存在严重的缺点:每次都遍历内存中所有的数据,非常消耗 CPU 资源,并且当 key 已过期,但是定时器还处于未唤起状态,这段时间内 key 仍然可以用。
- 惰性删除策略:在获取 key 时,先判断 key 是否过期,如果过期则删除。这种方式存在一个缺点:如果这个 key 一直未被使用,那么它一直在内存中,其实它已经过期了,会浪费大量的空间。
- 这两种策略天然的互补,结合起来之后,定时删除策略就发生了一些改变,不在是每次扫描全部的 key 了,而是随机抽取一部分 key 进行检查,这样就降低了对 CPU 资源的损耗,惰性删除策略互补了为检查到的key,基本上满足了所有要求。但是有时候就是那么的巧,既没有被定时器抽取到,又没有被使用,这些数据又如何从内存中消失?没关系,还有内存淘汰机制(保证Redis中存放的都是热点数据),当内存不够用时,内存淘汰机制就会上场。淘汰策略分为:
- 当内存不足以容纳新写入数据时,新写入操作会报错。(Redis 默认策略)
- 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。(LRU推荐使用)
- 当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。
- 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。
- 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。
- 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。
如何解决 Redis 缓存雪崩问题
缓存雪崩: 缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
- 缓存时间不一致,给缓存的失效时间,加上一个随机值,避免集体失效
- 使用 Redis 高可用架构:使用 Redis 集群来保证 Redis 服务不会挂掉
- 限流降级策略:有一定的备案,比如个性推荐服务不可用了,换成热点数据推荐服务
如何解决 Redis 缓存穿透问题
缓存穿透: 缓存穿透是指缓存和数据库中都没有的数据。
- 在接口做校验
- 存null值
- 设置过滤器拦截: 将所有可能的查询key 先映射到布隆过滤器中,查询时先判断key是否存在布隆过滤器中,存在才继续向下执行,如果不存在,则直接返回。布隆过滤器将值进行多次哈希bit存储,布隆过滤器说某个元素在,可能会被误判。布隆过滤器说某个元素不在,那么一定不在。
如何解决 Redis 缓存击穿问题
缓存击穿: 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
- 设置热点数据永远不过期
- 加互斥锁。缓存失效的时候,先锁住,等有缓存了,再解锁
- 接口限流与熔断、降级
4. Redis和MySQL的双写一致性
5. 从海量Key里查询出某一固定前缀的Key
假如Redis里面有一亿个key,其中十万个key是以某个固定已知的前缀开头的,如何将它们全部找出来?
摸清数据规模,即问清楚边界。
- KEYS pattern:查找所有符合给定模式pattern的key。由于keys一次性返回所有的key,如果key的数量过大,会导致客户端被卡住。Redis中的key非常多的时候,对内存的消耗和Redis服务器都是一个隐患。
- Scan指令,可以无阻塞的提取出指定的默认的key列表,scan每次执行只会返回少量元素,所以可以用于生产环境,而不会出现像keys命令带来的可能会阻塞服务器的问题。
Scan指令模式如下所示:
- SCAN cursor [MATCH pattern] [COUNT count]:基于游标cursor的迭代器,需要基于上一次的游标延续之前的迭代过程。以0作为游标开始一次新的迭代,直到命令返回游标0完成一次遍历。(当scan指令的游标参数即cursor被置为0的时候,服务器将开始一次新的迭代,而当服务器向用户返回值为0的游标的时候,就表示迭代完成。以0作为游标开始新一次的迭代,一直调用scan指令直到命令返回游标0,称这个过程为一次完整的遍历。)
- 不保证每次执行都返回某个给定数量的元素,支持模糊查询。(Scan增量式迭代命令并不保证每次执行都会返回某个给定数量的元素,甚至可能返回0个元素,但只要命令返回的游标不是0,应用程序就不应该将迭代视作结束,命令返回的元素数量总是符合一定的规则的。对于一个大数据集来说,增量式迭代命令每次最多可能会返回数十个元素,而对于一个足够小的数据集来说,可能会一次迭代返回所有的key,类似于keys指令,scan可以通过给定match参数的方式传入要查找键位的模糊匹配方式,让命令只返回给定模式下相匹配的元素。)
- 一次返回的数量不可控,只能是大概率符合count参数。(对于增量式迭代命令是没有办法保证每次迭代所返回的元素数量的,我们可以使用count选项对命令的行为进行一定程度的调整,count选项的作用就是让用户告知迭代命令,在每次迭代中,应该从数据集里返回多少元素,使用count选项对于增量式迭代命令相当于是一种提示,大多数情况下,这种提示都是比较有效的控制返回的数量。值得注意的是,count数量并不能严格的控制返回的key的数量,只能说是一个大致的约束,并非每次迭代都会返回count数量的约束,用户可以根据自己的需求在每次迭代中随意改变count的值,只要记得将上次迭代返回的游标用到下次迭代的游标里面就可以了。)
- 可能会获取到重复key,在程序中进行处理。
6. 如何实现分布式锁
分布式锁是控制分布式系统或者不同系统之间共同访问共享资源的一种锁的实现,如果不同的系统或者同一个系统不同主机之间共享了某个资源的时候,往往需要互斥来防止彼此干扰,进而保证一致性。
分布式锁需要解决的问题
- 互斥性:任意时刻只能有一个客户端获取锁,不能同时有两个客户端获取到锁。
- 安全性:锁只能被持有该锁的客户端删除,不能由其它客户端删除掉。
- 死锁:获取锁的客户端因为某些原因而宕机,而未能释放锁,其它客户端再也无法获取到该锁,而导致的死锁,此时需要有机制避免这种问题的发生 。
- 容错:当部分节点宕机的时候,客户端仍然能够获取锁和释放锁。
如何通过Redis实现分布式锁?
SETNX key value:如果key不存在,则创建并赋值。
时间复杂度:O(1)
返回值:设置成功返回1,设置失败返回0。
正因为SETNX的操作是原子性的,因此初期便被用在实现分布式锁。在执行某段代码逻辑的时候,先尝试使用SETNX对某个key设值,如果设值成功,则证明此时没有别的线程在执行该段代码,或者说占用该独占资源,这个时候线程就可以顺利的去执行该段代码逻辑了;如果设值失败,则证明此时有别的程序或者线程占用该资源,那么当前线程就需要等待直至设置SETNX成功。如果设置SETNX的key,这个key就会长久有效了,后续线程如何能再次获得到锁,此时需要给该key设置一个过期时间。
如何解决SETNX长期有效的问题?
EXPIRE key seconds
- 设置key的生存时间,当key过期时(生存时间为0),会被自动删除。使用EXPIRE设置过期时间的缺点就是原子性得不到满足。如果SETNX与EXPIRE结合使用,它们分别都是原子性的,但是组合到一起却不是原子性的。
- Redis2.6.12之后,通过set原子操作将SETNX和EXPIRE揉到一起去执行。
- SET key value [EX seconds] [PX milliseconds] [NX | XX]
- EX seconds,设置键的过期时间为second秒。
- PX milliseconds,设置键的过期时间为millisecond毫秒。
- NX,只在键不存在的时候,才对键进行设置操作。效果等同于SETNX。
- XX,只在键已经存在的时候,才对键进行设置操作。
- SET操作成功完成时候,返回OK,否则返回nil。
7. 如何使用Redis做异步队列?
使用List作为队列,rpush生产消息,lpop消费消息
在这种生产者和消费者的模式里面,当lpop没有消息的时候,说明消息暂时被消费完毕,并且生产者还没有来得及生产数据。
缺点:没有等待队列里面有值就直接消费(lpop是不会等待队列里有值才会去消费的)。
弥补:可以通过在应用层引入sleep机制去调用lpop重试。进而实现一个简单的异步队列。
BLPOP key [key…] timeout
如果不想使用sleep重试,可以使用blpop的方式。
阻塞直到队列有消息或者超时。在没有消息的时候会阻塞住,直到消息的到来或者超时。blpop可以替代sleep做更精准的阻塞控制。
缺点:只能供一个消费者消费,lpop或者blpop之后就没了。
pub/sub,主题订阅者模式
是否可以只生产一次,就让多个消费者消费呢?可以使用redis的pub/sub,主题订阅者模式,实现一对多的消息队列。
发送者pub发送消息,订阅者sub接收消息。
订阅者可以订阅任意数量的频道(topic,消费者关注的主题)。pub/sub的缺点:消息的发布是无状态的,无法保证可达。无法保证消息是否被接收到,是否在传输过程中丢失。对于消费者来说,消息是即发即失的。若某个消费者在生产者发布消息时下线,重新上线后接收不到该消息。要解决这个问题需要专业的消息队列,如Kafaka。
8. Redis如何做持久化?
Redis提供了三种持久化的方案,将内存中的数据保存到磁盘中,避免数据丢失。
- 基于BGSave的RDB快照持久化方式
特定的间隔保存那个时间点的全量数据快照。RDB相关的配置在redis根目录下的redis.conf中。redis服务启动时,会自动加载redis.conf中的信息。
redis.conf中的主要配置:
1)RDB持久化的时间策略举例:save 900 1指900秒内有一条写入指令就促发产生一次快照。产生一次快照就理解为进行一次备份。
2)stop-writes-on-bgsave-error yes指备份进程出错时,主进程停止接收新的写入操作。
3)rdbcompression yes和rdb文件压缩相关,设置成yes表示在备份时对rdb文件进行压缩后才去做保存。建议设置为no,redis本身属于cpu密集型服务器,开启压缩会带来更多的cpu消耗,相比硬盘成本,cpu更值钱。禁用rdb配置在save后加一行save “”。
src目录下有dump.rdb文件,表明当前选择的方式是rdb方式。dump.rdb是一个二进制文件。
RDB的创建与载入
RDB文件可以通过以下两个命令来生成:
1)SAVE,阻塞Redis的服务器进程,直到RDB文件被创建完毕。SAVE很少被使用,因为save操作是在主线程中保存快照的,由于Redis是用一个主线程来处理所有的请求的,这种方式会阻塞所有的客户端请求。
2)BGSAVE,fork出一个子进程来创建RDB文件,记录接收BGSAVE当时的数据库状态,父进程继续处理接收到的命令,子进程完成文件的创建之后会发送信号给父进程即Redis的主进程,而于此同时,父进程处理命令的同时,通过轮询来接收子进程的信号,不阻塞服务器进程。BGSAVE指令是使用后台方式保存RDB文件,调用此命令后会立刻返回OK返回码,Redis会产生一个子进程进行处理,并立刻恢复对客户端的服务,在客户端可以使用last save这个指令,查看操作是否成功,last save记录了上一次成功执行save或者bgsave命令的时间。
触发rdb持久化的方式
可以使用java计时器或者quartz来定期调用redis的bgsave指令去备份rdb文件,并按照时间戳存储不同的rdb文件,作为redis某段时间的全量备份脚本。
除了上面的手动方式触发rdb持久化,还有自动化触发RDB持久化的方式。自动触发的场景主要有以下几点:
1)根据redis.conf配置里面的save m n规则定时触发,这里面的save使用的是bgsave异步备份。
2)主从复制的时候,主节点自动触发。主节点发送RDB文件给从节点完成复制操作,主节点就会触发bgsave。
3)执行Debug Reload。
4)redis服务执Shutdown且没有开启AOF持久化。
BGSave的原理
先检查当前主进程有没有AOF或RDB子进程,有返回错误,防止子进程间的竞争。意味着在执行BGsave期间客户端发送的save、bgsave命令会被服务器拒绝执行。如果此时没有发现相关子进程,则会促发持久化,调用redis源码中的rdbSaveBackground方法,执行fork系统调用。操作系统fork指令用来创建进程。Linux下fork系统调用实现了Copy-on-Write,写时复制。传统方式下fork函数创建子进程时直接把所有资源复制给子进程,实现方式简单,但效率低下,而且复制的资源可能对子进程毫无用处。Linux为了降低创建子进程的成本,改进fork实现方式,当父进程创建子进程时,内核只为子进程创建虚拟空间,父子两个进程使用相同的两个物理空间,只有父子两个进程发生更改时,才会为子进程分配独立的物理空间。
Copy-on-Write(简称COW)写时复制,是计算机程序设计领域的优化策略,核心思想是如果有多个调用者同时要求相同资源(如内存或者磁盘上的数据存储),它们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容的时候,系统才会真正复制一份专用副本给该调用者,而其它调用者所见到的最初的资源仍然保持不变。这个过程对其他的调用者是透明的。此做法的主要优点是如果调用者没有修改该资源,就不会有副本被创建,多个调用者只是读取操作时可以共享同一份资源。
COW在处理过程中需要维持一个为读请求使用的指针,并在新数据写入完成后更新这个指针,以提升读写并发能力。因此COW也间接提供了数据更新过程中的原子性,在保证完整性的同时还保持读写效率。当redis需要持久化时,redis会fork一个子进程,子进程将数据写入磁盘上一个临时的rdb文件中,当子进程完成写临时文件时,将原来的rdb替换掉。这样的好处是实现copyOnWrite,子进程继续接收其他请求,确保了redis的性能。当redis需要做持久化时,redis就会调用fork创建子进程,父进程继续处理client请求,子进程负责将内存内容写入到临时文件中,父子进程共享相同的物理页面,父进程处理写请求时,OS为父进程要修改的页面创建副本,而不是写共享的页面。子进程第一次空间内的数据是fork时刻整个数据库的一个快照。当子进程完成临时空间的写入后,用临时文件替换掉原来的快照文件,子进程退出,完后一次备份操作。
RDB文件的载入一般是自动的,redis服务器再次启动时,如果检测到RDB文件的存在,redis会自动载入这个文件。
RDB持久化的缺点
1)内存数据的全量同步,数据量大会由于I/O而严重影响性能的。每次快照持久化都是将快照数据完整的写入到磁盘一次,并不是增量的只同步脏数据,如果数据量大的话,并且写操作比较多的时候必然会引起大量的磁盘IO操作,可能会严重影响性能。
2)可能会因为Redis挂掉而丢失从当前至最近一次快照期间的数据,由于快照方式是在一定间隔时间做一次的快照后的所有修改,如果应用要求不能丢失任何修改的话,可以采用AOF。
2)AOF增量持久化方式
AOF(append-only-file)持久化,通过保存Redis服务器所执行的写状态来记录数据库的。
记录下除了查询以外的所有变更数据库状态的指令。RDB持久化备份数据库状态;AOF持久化是备份数据库接收到的指令,所有被写入AOF的命令都是以redis协议格式来保存的。
在AOF中,以append的形式追加保存到aof文件中,以增量的形式。数据库会记录下所有变更数据库状态的指令,除了指定数据库的查询命令,其它的命令都是来自client的。
aof持久化默认是关闭的,可以通过修改refis.conf的配置appendonly no修改为appendonly yes即可,生成的文件名称是appendonly.aof。
appendfsync everysec该配置主要用来配置aof文件的写入方式的,可以接收三个不同的参数,分别是always、everysec、no。always表示一旦缓存区的内容发生变化,就总是及时的将缓存区的内容写入到aof中;everysec是将缓存区的内容每隔一秒去写入到aof中;no是将写入aof的操作交由操作系统来决定。一般而言,为了提高效率,操作系统会将缓存区被填满才会开始同步数据到磁盘中。一般推荐everysec默认的方式,速度比较快,安全性比较高。修改配置需要重启redis服务器。
AOF日志文件是一个纯追加的文件,就算遇到突然断电也可以尽最大权力去保证数据的无损。
日志重写(rewrite)解决AOF文件大小不断增大的问题,Redis支持在不中断服务的情况下,在后台重建AOF文件,原理如下:
1)首先调用fork(),创建一个子进程。
2)子进程把新的AOF写到一个临时文件里面,新的AOF的重写是直接把当前内存的数据生成对应的命令,并不需要读取老的AOF文件进行分析或者合并,不依赖原来的AOF文件。
3)主进程持续将新的变动同时写到内存和原来的AOF里面。这样即使写入失败,也能保证数据的安全。
4)主进程获取子进程重写AOF的完成信号,往新AOF同步增量变动。
5)使用新的AOF文件替换掉旧的AOF文件。
10. Redis数据的恢复
RDB和AOF文件共存情况下的恢复流程
RDB和AOF的优缺点?
- RDB优点:RDB本质上是一个内存快照,保存了创建RDB文件那个时间点的Redis全量数据,全量数据快照,文件小,创建恢复快。缺点:无法保存最近一次快照之后的数据。
- AOF优点:AOF本质上是一份执行日志,保存所有被Redis更改的指令,可读性高,适合保存增量数据,数据不易丢失。缺点:文件体积大,恢复时间长。
- RDB-AOF混合持久化方式
- Redis4.0之后,推出了结合AOF和RDB的混合模式,并且作为默认的方式来使用。即使用RDB作为全量备份,AOF作为增量备份,来提升备份的效率。
- AOF重写机制,也是先写一份全量数据到AOF文件中,再追加增量,只不过全量数据是以redis命令格式写入的,那么是否可以先以RDB格式写入全量数据,再追加增量数据呢,这样既可以提高读写和恢复速度,也可以减少文件的大小,还同时可以保证数据的完整性,能够结合RDB和AOF的优点,AOF和RDB的混合模式正是在这种需求下诞生的。在此种方式下,子进程在做AOF重写时,会通过管道从父进程读取增量数据并缓存下来,那么在以RDB格式保存全量数据的时候,也会从管道读取数据,同时不会造成管道的阻塞。也就是说,AOF文件前半段是RDB格式的全量数据,而后半段是Redis命令格式的增量数据。
- 总结,BGSAVE做镜像全量持久化,AOF做增量持久化,因为BGSAVE会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据的问题,需要AOF配合使用,在Redis重启的时候会使用BGSAVE持久化文件,重新构建内容,再使用AOF重放近期的操作指令,来实现完整恢复之前的状态。
11. Redis高可用
主从同步
pipeline
pipeline和Linux管道类似。
- Redis基于请求/响应模型,单个请求处理需要一一应答。同时需要执行大量命令,需要等待上一行命令应答后再执行后面的命令。这中间不仅有来回交互的时间,而且还频繁调用系统IO,发送网络请求。
- pipeline批量执行指令,节省多次IO往返时间。为了提升效率,pipeline一次发送多行命令,不需要等待上一行命令执行的结果。客户端将执行的命令写入缓存中,一次性发给Redis。pipeline可以将多次IO往返的时间缩减为1次,前提是pipeline指令间没有依赖的相关性。有顺序依赖的指令建议分批发送。
Redis的同步机制
redis正常部署中都是由一个master进行写操作,其他若干个slave进行读操作。master和slave分为一个个独立的redis server实例。为了支持数据的弱一致性(最终一致性),不需要实时保证master和slave之间的数据是同步的,但是过一段时间数据是趋于一致性的,即所谓的最终一致性。redis可以使用主从同步和从从同步,第一次同步时主节点bgsave,同时将后续修改操作记录到内存buff里面,待完成后将RDB文件全量同步到从节点里面。从节点接受完成后将RDB镜像加载进内存中,加载完成后再通知主节点将期间修改的操作记录即增量数据同步到从节点进行存放。到某个时间点前的数据同步完后该时间点之后的增量数据也去进行存放,完成整个同步的过程。
按照同步内容的多少分为全同步和增量同步
- 全同步流程
- Slave发送sync命令到Master。
- Master启动一个后台进程,将Redis中的数据快照保存到文件中,即BGSave。
- Master将保存数据快照期间接收到的写命令缓存起来。
- Master完成写文件操作后,将该文件发送给Salve。
- Slave接收文件后将文件保存到磁盘中,加载到内存中恢复数据快照。(使用新的AOF文件替换掉旧的AOF文件)
- Master将这期间收集的增量写命令发送给Slave端。
- 全量同步操作完成后后续所有写操作都是在Master上进行,读操作都是在Slave上进行。Master也可以读,一般为了提升性能将读操作放到Slave上。因此用户的写操作需要及时扩散到Slave,以便保持数据最大程度上同步。Redis的Master、Slave进程在正常运行期间更新操作,包括写、删除、更改操作。
- 增量同步过程
Redis的主从进程在正常运行期间更新操作的增量同步方式如下:
- Master接收到用户的操作指令,判断是否需要传播到Slave。增删改需要扩散到Slave,查不需要。
- 将操作记录追加到AOF文件。将操作转换为Redis内部的协议格式,并以字符串的形式存储。将字符串存储的操作追加到AOF之后
- 将操作传播到其他Slave:a)对齐主从库;b)将命令参数按照redis协议格式写入响应Slave的缓存中
- 将缓存中的数据发送给Slave
Redis Sentinel
主从模式的弊端是不具备高可用性,当Master挂掉后Redis将不能对外提供写入操作。Redis Sentinel应运而生。Redis Sentinel即Redis哨兵,是Redis官方提供的集群管理工具,其本身也是一个独立运行的进程,能监控多个Master、Slave集群,发现Master宕机后能自动切换。主要功能如下:
- 监控:检查主从服务器是否正常运行
- 提醒:通过API向管理员或者其他应用程序发送故障通知。
- 自动故障迁移:主从切换。
Redis Sentinel是一个分布式系统,可以在一个架构中运行多个Sentinel进程。这些进程使用流言协议Gossip接收关于主服务器是否下线的信息,使用投票协议决定是否执行自动故障迁移,以及选择哪个从服务器作为新的主服务器。跟Zookeeper比较类似。
流言协议Gossip
在杂乱无章中保持一致。每个节点都随机地与对方通信,最终所有节点的状态达成一致。种子节点定期随机向其他节点发送节点列表以及需要传播的信息。不保证信息一定传递给所有节点,但是最终会趋于一致。
12. Redis集群
在网站承受高并发访问压力的同时,如何从海量数据里找到所需,并快速响应?
分片:按照某种规则去划分数据,分散存储在多个节点上。
通过实现数据分片,降低单节点服务器压力。
Redis集群采用无中心结构,每个节点保存数据和整个集群的状态,每个节点都和其他所有节点连接。节点之间使用Gossip协议传播信息以及发现新的节点。
Redis集群的目的是将不同的key分散放置到不同Redis节点,实现原理如下:
获取key的hash值,根据节点数求模。动态增加或减少节点时,会造成大量key无法被命中。为了解决这个问题,redis引入了一致性hash算法。
- 一致性hash算法:对2^32取模,将hash值空间组织成虚拟的圆环。各个服务器使用hash进行hash变换,具体可以选择服务器ip或者主机名作为关键字进行hash,这样每台服务器可以确定在hash环上的位置。对数据使用同样的hash算法定位到相应的服务器。沿环顺时针行走,第一台遇到的服务器就是数据的目标存储服务器。
- 在一致性hash算法中,如果一台服务器不可用,受影响的仅仅是此服务器到其环空间中前一台服务器(逆时针方向)之间的数据,其他数据不会受影响。并且该路径上新增的数据会存储到离它顺时针方向最近的节点上。做到最小化的有损服务。
- 综上所述,一致性hash算法对节点的增减都只需要重新定位环中一小部分数据,具有较好的容错性和扩展性。
hash环的数据倾斜问题:一致性hash算法在服务器节点很少时,容易因为节点分布不均匀造成数据倾斜,被缓存的对象大部分集中在某一台服务器上。为解决数据倾斜问题,一致性hash算法引入虚拟节点。对每个服务器节点计算多个hash,计算结果位置放置一个子服务器节点,称为虚拟节点。具体做法可以在服务器ip或主机名后增加编号来实现。在实际应用中,通常将虚拟节点设置为32甚至更大,因此即使很少服务节点,也能做到相对均匀的数据分布。结合redis集群技术,还可以在期间引入主从同步、redis哨兵机制进一步确保集群的高可用性。
参考:redis高可用方案
一、Java底层知识
1.Java特性理解
平台无关性
GC
语言特性
面向对象
类库
异常处理
平台无关性:
指令 | 含义 |
---|---|
javac | 将.java文件编译成class字节码文件 |
java | 运行class文件 |
javap -c | 将class文件反编译 |
2.JVM如何加载.class文件
3.Java反射
Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。
4.什么是ClassLoader
自定义ClassLoader 实例:
Chao:所需要加载的类
public class Chao {
static{
System.out.println("自定义的类加载。。。运行");
}
}
MyClassLoader:用于加载类
public class MyClassLoader extends ClassLoader {
private String path;
private String classLoaderName;
public MyClassLoader(String path, String classLoaderName) {
this.path = path;
this.classLoaderName = classLoaderName;
}
//用于寻找类文件
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
//用于加载类文件
private byte[] loadClassData(String name) {
name = path + name + ".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(new File(name));
out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
}
ClassLoaderChecker:main入口运行
public class ClassLoaderChecker {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader m = new MyClassLoader("/Users/mac/lenovo/src/test/java/domain", "myClassLoader");//第一个参数是Chao.java文件所在目录
Class c = m.loadClass("Chao");
System.out.println(c.getClassLoader());
System.out.println(c.getClassLoader().getParent());
System.out.println(c.getClassLoader().getParent().getParent());
System.out.println(c.getClassLoader().getParent().getParent().getParent());
c.newInstance();
}
}
5.ClassLoader的双亲委派机制
为什么要使用双亲委派机制加载类:避免多份同样字节码的加载
6.LoadClass和forName区别
类的加载方式:
隐式加载:new
显式加载:loadClass,forName等
loadClass和forName的区别:
Class.forName得到的class是已经初始化完成的
ClassLoader.loadClass得到的class是还没有链接的
7.Java内存模型
二、Spring
AOP
在这里插入图片描述