4.1说说Redis是怎么保证高可靠性的?

Redis具有高可靠性主要有两层含义:
(1)数据尽量少丢失:AOF和RDB保证了前者;
(2)服务尽量少中断:增加Redis实例,增加副本冗余量。
Redis提供了主从库模式,保证数据副本的一致性,主从库之间采用的是读写分离的方式。读操作是主库、从库都可以接收。写操作首先到主库执行,然后主库同步给从库。90097d01-5dea-46c6-bebd-b5746fb5b0fd.png
【为什么要采用读写分离的方式呢?】
假设,不管是主库还是从库,都能接收客户端的写操作,那么直接的问题就是如果客户端对同一个数据修改了三次,每一个写操作都分发到不同的Redis实例上,在不同的实例上执行,那再读取这个数据的时候可能就会读取到旧的值。如果非要保持这三个实例上的数据一致,就要涉及到加锁、实例间协商是否完成修改等操作,这会带来巨大的开销。而主从库模式一旦采用了读写分离,所有数据的修改只会在主库上进行,不用协调三个实例。主库有了最新的数据后,会同步给从库,这样,主从库的数据就是一致的。
【主从库之间如何进行第一次同步?】
当我们启动多个Redis实例的时候,它们之间就可以通过replicaof命令形成主库和从库的关系。
image.png
(1)从库发送psync命令,表示要进行数据同步,psync命令包含了主库的runID和复制进度offset两个参数,第一次复制时,不知道主库的runID所以会用?来表示,offset为-1,表示第一次复制。主库收到psync命令后,会用fullResync响应命令带上主库的RunID和主库的复制进度offset返回给从库。FullResync响应表示第一次复制采用的全量复制,主库会把当前所有的数据都复制给从库。
(2)从库收到数据后,在本地完成数据加载,这个过程依赖于内存快照RDB文件。具体来说就是主库执行bgsave命令,生成RDB文件,接着把文件发送给从库。从库接收到RDB文件后,为了避免之前数据的影响,会先清空当前数据库,然后加载RDB文件。在主库将数据同步给从库的过程中,主库不会被阻塞,依然可以正常接收请求,为了保证主从库的数据一致性,主库在内存中有专门的replication buffer,记录RDB文件生成后收到的所有写操作。
(3)主库完成RDB文件发送后,会把replication buffer中的修改操作发送给从库,从库再执行这些操作。
【主从级联模式分担全量复制时主库压力】
在做全量复制时,对于主库来说,需要完成两个耗时的操作分别是生成RDB文件和传输RDB文件,如果从库数量很多,而且都要和主库进行全量复制的话,就会导致主库忙于fork子进程生成RDB文件,进行全量数据同步,fork操作会阻塞主线程正常处理请求,从而导致主库的响应请求速度变慢。所以就可以使用到主从从模式了。
在部署主从集群的时候,手动选择一个从库(比如内存资源配置较高的从库),用于级联其他的从库,其他的从库与这个从库负责建立连接和数据同步。这样主从值需要和这一个从库进行交互就可以了。就可以减轻主从的压力。
image.png
【主从库网络断连了怎么办?】
在Redis2.8以后,如果主从和从库网络断连,主从库之间会采用增量复制的方式继续同步。增量复制的方式只会把断连期间内主库收到的所有写命令,同步给从库。
具体来说,主从库断连以后,主库把断连期间收到的所有写操作命令,写入到replication buffer中,同时也会把这些命令到入到repl_backlog_buffer这个缓冲区。repl_backlog_buffer是一个环形缓冲区,主库会记录自己写到的位置,从库会记录自己已经读到的位置。通常用偏移量来衡量,对主库来说,对应的偏移量就是master_repl_offset,从库对应的偏移量是slava_repl_offset,主从的偏移量随着读写操作的增加会越来越大,从库则是在于主库完成数据同步以后这个偏移量会越来越大。主从库恢复连接之后,从库发送psync命令给主库,并发自己的slave_repl_offset发送给主库,主库会判断自己的master_repl_offset和slava_repl_offset之间的差距,所以主库只需要把这之间的差值的命令部分同步给从库就行了。
image.png
因为repl_backlog_buffer是一个环形缓冲区。所以在缓冲区写满后,主库会继续写入,此时,就会覆盖掉之前写入的操作。如果从库的读取速度比较慢,就会可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库的数据不一致。所以就需要调整repl_backlog_size这个参数,这个参数和所需的缓冲空间大小有关。如果配置得过小,就会导致从库的复制进度赶不上从库,进而在重连后需要进行全量同步。所以调大这个参数可以减少主从库网络断连时需要全量同步的风险。
另外虽然全量复制比较耗时,但如果是第一同步的话是无法避免的,所以一个Redis的实例不要太大,一般在几GB级别比较合适,这样也可以减少RDB文件生成、传输、重新加载的开销。

4.2主库挂了,如何不间断服务?

在主从库模式下,如果从库发生故障了,还可以通过psync命令与主库重新进行连接并进行数据同步,但如果主库挂了,就会直接影响到从库的同步,因为从库没有相应的主库可以进行数据复制操作了。如果客户端发生的都是读操作,那还可以由从库继续提供服务,但一旦有写操作,就没有实例来响应客户端的写请求了。
image.png
所以如果主库挂了, 就需要选择一个从库,将它切换成主库继续响应客户端的写请求。
【哨兵机制的基本流程】
哨兵就是运行在特殊模式下的Redis进程,主从库实例运行的同时,它也在运行。哨兵主要负责的是:监控、选主、通知。
【监控】:是指哨兵进程在运行时,周期性地给所有主从库发送PING命令,检测他们是否仍然在运行。如果从库没有在规定时间内响应哨兵的PING命令,哨兵就会把它标记为下线状态;同样,如果主库没有在规定时间内响应哨兵的PING命令,稍微就会判定主库下线,然后开始自动切换主库的流程。
在实际生产环境中,我们一般会采用多实例的集群模式组成哨兵集群,组成哨兵集群的原因是减少单个哨兵实例对主库下线的误判。误判是指在主库本身压力较大的情况下,或者是网络拥塞、网络压力较大的情况下,主库不能及时响应哨兵的PING命令,导致被哨兵标记为下线,实际上主库并没有真正的下线。所以一旦误判就要重新选主,通知客户端和从库,让从库与新主库进行同步,这个过程又会带来很大的开销。
所以如果部署哨兵集群的话,由多个哨兵检测主库是否真正下线来减少误判。具体来说就是,哨兵集群周期性地向主库发送PING命令,主库需要响应多个哨兵实例的PING命令,如果在一定时间内主库没有响应哨兵的PING命令,就会被当前哨兵标记为主观下线,打个比方,如果部署了3个哨兵实例组成集群,如果2个哨兵实例判断主库主观下线,那主库就会被标记为客观下线,少数服从多数。所以部署哨兵集群最好是部署成奇数个。
【选主】:如果主库被哨兵集群标记为客观下线,哨兵就会按照一定的规则来从从库中选择一个作为新主库。选择新主库的过程包含筛选和打分两个阶段。
筛选:这个过程需要检查从库的当前在线状态,以及网络连接状态。如果在一定的时间内,主从库之间没有取得连接,就可以认为主从库之间发生了断连,如果断连次数超过了10次就可以认为从库的网络连接状态不好,不适合作为新主库,就会直接被哨兵给筛选掉。
打分:打分会按照三个规则依次进行三轮打分,分别是从库优先级、从库复制进度、从库ID号。
从库优先级:这个可以通过slava-priority参数进行配置,给从库设置不同的优先级,优先级的高的从库会会打高分,优先级低的会打低分,如果有一个从库优先级最高,那它就是新主库了,如果都一样,就进行下一轮的打分。
从库复制进度:需要选择和旧主库同步进度最为接近的那个从库作为新主库。master_repl_offset会记录旧主库的最新写操作位置,而slave_repl_offset会记录从库的复制进度,所以会选择从库中slave_repl_offset最接近master_repl_offset的那个从库作为新主库。如果都一样还会进行下一轮打分。
从库ID号:从库ID号小的打高分,作为新主库。
【通知】:哨兵集群中的Leader负责通知客户端以及其他从库新主库的信息。

4.3哨兵如果挂了,主从库如何切换?

实际上,一旦多个实例组成了哨兵集群,即使有哨兵实例出现故障挂掉了,其他哨兵也还能够继续协作完成主从库切换的工作。包括监控、选主、通知。
【基于pub/sub机制的哨兵集群】
哨兵实例之间之所以可以互相发现,要归功于Redis提供的pub/sub机制,发布订阅机制。哨兵只要和主库简历起了连接,就可以在主库上发布消息了,比如发布它自己的连接信息(IP以及端口号)。同时它也可以在主库上订阅消息,获得其他哨兵发布的连接信息。当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的IP地址和端口。
image.png
【哨兵如何知道从库的IP地址和端口?】
哨兵向主库发送INFO命令来完成的,主库接收到这个INFO命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续对从库进行监控。
image.png
【哨兵与客户端建立连接】
哨兵是运行在特定模式下的Redis实例,只不过它并不服务请求操作,客户端可以通过读取哨兵的配置文件获得哨兵的地址和端口,和哨兵建立网络连接,然后再客户端执行订阅命令,来获取不同的事件消息。
【由哪个哨兵执行主从切换】
具体由哪个哨兵执行主从切换的过程,和主库客观下线的判断过程类似,也是一个投票仲裁的过程。哨兵集群中任何一个实例只要自身判断主库主观下线,就会给其他实例发送is-master-down-by-addr命令。接着,其他实例会根据自己和主库的连接情况,做出Y和N的响应,Y相当于赞成票,N相当于反对票。
image.png
一个哨兵获得仲裁所需的赞成票数后,就可以判断主库客观下线了。票数可以通过quorum配置项来进行配置,或者默认是n/2+1的票数,就可以判断主观客观下线了。此时,这个哨兵就可以再给其他哨兵发送命令,表明希望由自己来执行主从切换,并让所有的其他哨兵来投票,这个过程称为Leader选举。在投票过程中,任何一个想成为 Leader 的哨兵,要满足两个条件:第一,拿到半数以上的赞成票;第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
image.png
当一个哨兵成为Leader的时候,由它来完成选主的过程,在选择完新主库后,负责通知客户端和其他从库需要与新主库建立连接。

4.4谈谈Redis的切片集群

打个比方,如果要用Redis保存5000万个键值对,每个键值对大约是512B,这些键值对所占用的内存空间大约是25GB。
【方案一】利用32GB内存的云主机来部署Redis这个方案是可行的,还剩有7GB可以保证系统的正常运行。同时,还能采用RDB对数据做持久化,以确保Redis实例故障后,还能从RDB恢复数据。但是Redis在使用RDB做持久化时,Redis会fork子进程来完成,fork操作的用时与Redis的数据量成正比,而fork会阻塞主线程,导致Redis的响应变慢。但这个方案的优点是,实施起来比较简单、直接。
【方案二】切片集群,启动多个Redis实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。如果把25GB的数据平均分成5份,使用5个实例来保存,每个实例都需要保存5GB内存。实例在为这5GB内存做持久化时,fork子进程的耗时就会短很多。另外一个原因是,之所以不使用纵向扩展是因为大内存的云服务器的价格比多个小内存的云服务器要贵得多,另外如果不是25GB呢,而是1TB的数据呢,那纵向扩展就很难办得到了。所以如果采用切片集群的方案,我们只需要使用5台8GB内存,50GB磁盘的云主机来保存这些数据就可以了。
【数据切片和实例的对应分布关系】
Redis Cluster方案采用哈希槽(Slot)来处理数据和实例之间的映射关系。一个切片集群共有16384个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的key,被映射到一个哈希槽中。具体的映射过程分为两步:
(1)首先根据键值对的key,按照CRC16算法计算一个16bit的值。
(2)然后在用这个16bit的值对16384取模,得到0-16383范围内的模数,每个模数代表一个相应编号的哈希槽。
我们在部署Redis Cluster方案时,可以使用cluster create命令创建集群,此时,Redis会自动会这些槽分布在集群实例上。例如,如果集群中有N个实例,那每个实例上的哈希槽个数为16384/N个。
image.png
【客户端如何定位数据?】
在定位键值对时,它所处的哈希槽是可以通过计算得到的,这个计算可以在客户端发送请求时执行。但是要进一步定位到实例,还需要知道哈希槽分布在哪个实例上。
一般来说,客户端与集群实例建立连接后,实例就会把哈希槽的分配信息发送给客户端。在集群刚刚创建的时候,每个实例只知道自己被分配了哪些哈希槽,不知道其他实例拥有的哈希槽信息,随后Redis实例会把自己的哈希槽信息发送给其他和它相连的其他实例,来完成哈希槽分配信息的扩散。客户端收到哈希槽信息后,会把哈希槽信息缓存到本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。
但是,在集群中,实例和哈希槽的对应关系不是一成不变的。在集群中,实例有新增或删除时,Redis需要重新分配哈希槽;为了负载均衡,Redis需要把哈希槽在所有实例上重新分布一遍。此时,实例之间可以通过相互传递消息,来获得最新的哈希槽分配信息。但是客户端是无法感知这些变化的。Redis Cluster提供了一种重定向机制,就是客户端给一个实例发送数据读写操作时,这个实例上并没有相应的数据,那这个实例就会给客户端返回MOVED命令响应结果,这个结果中包含了新实例的访问地址。随后客户端修改自己缓存中Slot与实例的对应关系。
另外还有一种情况是如果数据正在迁移的过程中,一部分数据迁移了,还有一部分尚未迁移。在这种情况下,客户端就会收到一条ASK报错信息,客户端需要向另外一个实例发送ASKING命令,意思是让这个实例允许客户端接下来发送的命令,然后客户端再向该实例发送GET命令。与MOVED命令不同的是,ASK命令不会更新客户端缓存的哈希槽分配信息。如果客户端再次请求,在迁移完成后还是会收到MOVED命令,这时候才能更新本地的哈希槽分配信息。