主从复制
读写分离,主库和从库都可以进行读操作;主库进行写操作,然后将数据同步给从库。
主从库第一次数据同步(全量复制)
- 建立连接,协商同步。为全量复制做准备,从库和主库建立起连接,并告诉主库即将进行同步,主库回复确认之后,主从库之间开始数据同步。
-
- 从库给主库发送
psync
命令,表示要进行数据同步,命令中包括主库的runID和复制进度offset两个参数,主库根据命令以及参数启动复制。
- 从库给主库发送
-
-
- runID:每个Redis实例启动时都会自动生成的一个随机ID,用来唯一标识这个实例,第一次复制时,从库不知道主库的runID,所以设为一个?。
- offset:复制进度,-1表示第一次复制
-
-
- 主库收到
psync
命令之后,在第一次数据同步的时候会用FULLRESYNC
响应带上主库的runID和主库目前的复制进度offset返回给从库,从库收到响应后会记录下这两个参数。
- 主库收到
- 主库同步数据给从库。
FULLRESYNC
响应表示第一次复制采用的全量复制,主库会把所有的数据同步给从库。
-
- 主库执行
bgsave
命令,生成RDB文件,将文件发送给从库 - 从库接收到RDB文件后,会将所有数据清空(防止从库中存在其他数据与主库不一致),然后再加载接收到的RDB文件
- 主库执行
- 主库发送新的写命令给从库。主库在执行
bgsave
的时候还可以正常接收请求,这些数据不会记录到生成的RDB文件中,为了保证主从库的一致性,主库中会有一个专门的replication buffer
,用来记录RDB文件生成后的所有写操作,当主库完成RDB文件发送后,就将此时的replication buffer
中的修改操作发给从库,从库再执行一遍这些操作。
主从级联模式
传统的主从模式中,主库要负责生成RDB文件以及传输RDB文件,这两个操作都是比较耗时的操作,如果从库过多,可能会造成主库的主线程阻塞,所以就有了主-从-从模式。
通过主-从-从模式,可以将主库生成RDB和传输RDB的压力,以级联的方式分散到从库上。也就是有的从库是从主库的从库中获取同步数据。
增量复制
一旦主从库完成了全量复制,两者之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销
Redis2.8开始,如果主从之间网络断连,主从库之间会采用增量复制的方式继续同步,也就是只需要同步网络断连期间主库收到的命令。
Redis通过环形缓冲区repl_backlog_buffer
来实现增量复制的功能
在主服务器进行命令传播的时候,不但将写命令发送给从服务器,同时会将写命令写入到repl_backlog_buffer
环形缓冲区,该缓冲区就会保存着主服务器最近传播的写命令。
网络断开后,当从服务器重新连接到主服务器时,会发送psync
命令,会带有slave_repl_offset
参数,主服务器收到之后会对比slave_repl_offset
和master_repl_offset
之间的差距
- 如果判断出数据还在环形缓冲区内,则进行增量复制,即将数据复制到
replication buffer
中发送给从服务器 - 如果数据不在环形缓冲区内,就进行全量复制
出现阶段 | 缓冲区满了之后 | |
replication buffer | 全量复制和增量复制阶段都会出现,主节点会给每个从节点都分配一个 | 会导致连接断开,删除缓存,从节点重新连接,重新开始全量复制 |
repl backlog buffer | 增量复制阶段出现,一个主节点只分配一个 | 环形缓冲区,直接覆盖旧数据 |
如何判断节点是否存活
Redis通过ping-pong
机制来判断节点是否存活,如果有一半的节点ping一个节点的时候没有pong返回,就认为该节点挂了,会断开与这个节点的连接。
对于主节点来说,每隔10秒对从节点发起ping命令,判断从节点的存活性和连接状态,可以通过repl-ping-slave-period
控制发送频率。
对于从节点来说,每隔1秒发送replconf ack{offset}
命令,给主节点上报自身当前的复制偏移量
- 实时检测主从节点网络状态
- 上报自身复制偏移量,检查复制数据是否丢失,如果从节点数据丢失,再从主节点的复制缓冲区拉取丢失的数据
过期key
主节点删除了一个key或者通过淘汰算法淘汰了一个key后,主节点会模拟一条del命令发送给从节点,从节点收到命令后就删除该key
主从不一致
- 尽量保证主从节点间的网络连接状况良好,避免主从节点在不同的机房
- 通过外部程序来监控主从节点之间的复制进度
-
- 可以通过Redis的
INFO replication
命令查看主节点接收写命令的进度以及从节点接收写命令的进度,通过计算两者的差值来监控复制进度 - 如果某个节点的进度差值大于阈值,可以让客户端不再从该节点读取数据
- 可以通过Redis的
主从切换
主从复制模型在主节点挂了的情况下,从节点无法自动升级为主节点。这个过程需要人工处理,在此期间Redis无法对外提供写操作
主从切换异步复制导致数据丢失
当客户端写入数据到主节点后,主节点还没来得及同步到从节点就发生了断电,那么主节点内存中的数据就会丢失。
- Redis有一个
min-slaves-max-lag
参数,表示一旦所有从节点数据复制和同步的延迟都超过了这个值后,主节点会拒绝任何请求,这样就控制了数据丢失的大小,只会丢失这个参数配置时间内的数据,减少了损失 - 客户端发现Redis写入失败之后,可以采取降级措施,比如写到本地缓存或者写入消息队列
集群脑裂
由于网络问题,集群节点之间失去联系。主从数据不同步,重新平衡选举,产生两个主节点。等网络恢复,旧的主节点会降级为从节点,再与新的主节点进行同步复制,此时由于会进行全量同步,会清空自己的缓冲区,这样前面客户端写入的数据就会丢失。
解决方案:
当主节点发现从节点下线的数量太多或者网络延迟太大的时候,主节点会禁止写入操作,直接把错误返回给客户端
min-slaves-to-write x
:主节点至少x个从节点,从节点小于这个数的时候,主节点会禁止写入操作min-slaves-max-lag x
:主从复制和同步延迟不能超过x秒,如果超过,主节点会禁止写入操作
哨兵机制
哨兵其实就是一个运行在特殊模式下的Redis进程,主从库实例运行的同时,哨兵也在运行。
哨兵职责
哨兵主要有以下三个任务:
- 监控
- 选主
- 通知
监控
哨兵进程在运行的时候,会周期性地给所有主从库发送ping命令,检测它们是否仍然在线运行。
- 如果从库没有在规定时间内响应哨兵的ping命令,哨兵就会把它标记为下线状态
- 如果主库没有在规定时间内响应哨兵的ping命令,哨兵就会判定主库下线,然后开始自动切换主库的流程
- 如果经过换主操作后旧的主节点重新上线,将其会设置为从节点
主观下线
哨兵进程会使用ping命令来检测自身和主从库的连接情况,用来判断实例的状态。
如果哨兵发现主从库对于ping命令的响应超时了,就会将该节点标记为主观下线
- 如果是从库被标记为主观下线,那么就直接下线就好,从库下线不会影响到整个集群的对外服务
- 如果是主库被标记为主观下线,考虑到主库下线的话就需要进行重新选主,重新选主后还要重新进行数据同步,所以为了防止主库被哨兵误判下线,所以进入哨兵集群,多个哨兵一起判断,防止因为某个哨兵的网络问题导致误判
客观下线
对于主库而言,一个哨兵判断其主观下线并不够,需要大多数的哨兵实例都认为主库已经主观下线了之后,才能判断主库已经客观下线了,此时才会触发哨兵的选主机制
客观下线的主要标准就是少数服从多数,如果有N个哨兵,一般当有N/2+1
个哨兵认为主库主观下线之后,该主库才被标记为客观下线
选主
主库挂了之后,哨兵需要从多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库
哨兵在所用从库中先进行筛选,把不符合条件的从库去掉,再按照一定的规则进行打分,将得分最高的从库选为新的主库。
筛选
在选主的时候,除了要检测从库当前的在线状态,还需要判断它之前的网络连接状态,如果这个从库之前就经常断连,那么就认为这个从库的网络状况存在问题,不适合成为主库。
可以通过配置项down-after-milliseconds
来判定主从库的最大连接时间,如果在配置时间内,主从库没有连接上,则认为断连,如果超过了10次,则认为从库的网络状况不好,不适合成为主库。
打分
通过三个规则,分别是从库优先级、从库复制进度、从库ID号进行三轮打分操作,只要在某一轮中的从库得分最高,则将其选为主库
- 优先级最高的从库得分高,可以通过
slave-priority
来设置从库的优先级,如果有一个从库的优先级高,则将其选为主库,如果都是一样的,则进行下一轮打分 - 与旧主库的复制同步最接近的得分高,通过主库的
master_repl_offset
和从库的slave_repl_offset
来计算主从库之间的同步差值,差值最小的成为主库,否则进行第三轮打分 - id小的得分高
通知
哨兵选出了新的主库之后,会把新的主库的连接信息发送给其他从库,让它们执行replicaof
命令,和新主库建立连接并进行数据复制。
同时,哨兵会把新主库的连接信息通知客户端,让它们把请求操作发到新的主库上。
哨兵集群
配置哨兵的命令
sentinel monitor <master-name> <ip> <redis-port> <quorum>
配置哨兵的时候只需要指定主库名称、主库ip、主库端口以及哨兵集群中多数哨兵同意主库下线的最小数量
哨兵发现哨兵
哨兵实例之间相互发现是基于Redis的pub/sub机制,也就是发布/订阅机制。
哨兵只要和主库建立起来了连接,就可以将自己的信息(IP和端口)发布在主库上,同时可以从主库上订阅消息,获取到其他哨兵的信息,这样所有的哨兵就彼此知道IP和端口了。
Redis的pub/sub机制通过频道来区分不同的消息。频道可以理解为消息的类别,只有同一类的消息才能在同一个频道里,只有订阅了相同频道的时候,才能够相互交换信息
在Redis的主从集群中主库上会有一个__sentinal__:hello
的频道,不同的哨兵通过这个频道实现相互通信。
哨兵发现从库
哨兵通过向主库发送INFO
命令来获取到从库的列表,然后基于这个列表与从库建立连接关系。
哨兵与客户端通信
哨兵从本质上说也是一个Redis实例,只是不对外提供服务,所以哨兵本身也有pub/sub机制,客户端可以通过订阅哨兵不同的频道来获取主库的情况。
客户端读取到哨兵的配置文件之后,可以获得哨兵的地址和端口,然后和哨兵建立起来网络连接,通过订阅哨兵提供的频道来获取主库的信息
主从切换的哨兵
哨兵集群通过一个投票仲裁的方式来决定由哪一个哨兵最终来执行换主的操作。
- 任何一个哨兵实例在认为主库主观下线之后,就会给其他的哨兵实例发送
is_master_down_by_addr
命令 - 收到
is_master_down_by_addr
命令的其他哨兵就会根据自己与主库的连接情况回复Y(赞成)或者N(反对) - 当一个哨兵收到了仲裁所需的赞成票(quorum配置决定的最小赞成票数)之后,就会将主库标记为客观下线
- 此时哨兵就可以发起Leader选举,也就是决定哪个哨兵来进行换主的操作,哨兵会发出命令表示自己想要成为Leader(可能会有多个哨兵发起),其他哨兵会进行投票选举,成为Leader的哨兵需要满足两个条件
-
- 拿到半数以上的赞成票
- 拿到的票数需要大于等于quorum配置的值
- 接下来被选举出来的Leader哨兵就会进行选主操作,如果没有选出Leader,则在等待一段时间(哨兵的故障转移时间的2倍)之后再次进行选举
在Leader选举中,只有标记主库为主观下线的哨兵才能成为候选人,候选人在Leader选举的时候会先给自己投一个赞成票,随后会发送命令给其他的哨兵,投票哨兵会给先收到的命令投赞成票,一轮投票中一个哨兵只有1票,所以后来的命令就只能投反对票了。
集群模式
分片集群,启动多个Redis实例组成一个集群,按照一定的规则将收到的数据划分成多份,每一份用一个实例来保存。
数据切片与实例
Redis Cluster采用哈希槽(Hash Slot)的方式来处理数据和实例之间的映射关系
在Redis Cluster中,一个分片集群一共有16384个哈希槽,哈希槽类似与数据分区,每个键值对都会根据它的key被映射到一个哈希槽里。
- 首先根据键值对的key,通过
CRC-16
算法生成一个16位的值 - 将这个值对16384取模,得到一个0-16383之间的数字,正好对应其中一个哈希槽
CRC(循环冗余校验)16算法是一种常用的错误检测算法,广泛应用于通信和数据存储系统中。CRC算法的核心思想是通过将数据视为一个大的多项式,然后除以另一个固定的多项式(称为生成多项式)来生成校验和。CRC-16指的是生成16位(即2字节)长度的校验和。
自动分配哈希槽
在部署Redis Cluster的时候,可以通过cluster create
命令创建集群,此时Redis会自动将16384个哈希槽平均分配到所有实例上,比如如果有N个实例,那么每个实例上就会分配16384/N个哈希槽
手动分配哈希槽
当我们通过cluster meet
手动创建集群的时候,可以通过cluster addslot
命令手动给实例分配哈希槽。一个集群必须分配够16384个哈希槽,负责集群无法工作。
客户端定位数据
- Redis实例会把自己的哈希槽信息传递给其他实例,完成哈希槽信息的扩散,当实例之间的连接建立起来之后,每个实例也就有了所有的哈希槽分配的情况
- 客户端与实例建立连接之后,实例就会将所有的哈希槽分配情况返回给客户端,客户端缓存了这个信息之后,客户端也就可以通过key来计算出数据在哪个实例上了
重定向机制
当集群中有新的实例增加、有实例被删除或者为了负载均衡时,Redis会对哈希槽进行重新分配,为了解决重新分配后的哈希槽信息与客户端一开始接收的不一致,Redis Cluster提供了重定向机制。
所谓重定向机制,就是客户端给一个实例发送读写操作的时候,如果这个实例上没有相应的数据,这个实例会给客户端返回一个带有新的实例地址的MOVED
命令,客户端需要再给新的实例发送操作命令,同时更新缓存的哈希槽的情况。
在Redis进行哈希槽重新分配的时候,有可能请求的数据槽正在进行迁移,但是没有迁移完成,比如说slot 1中有key1和key2,key1已经迁移到了slot 2,此时客户端来slot 1请求key1,这个时候实例会返回ASK
命令给客户端,里面还是会带有slot 2的地址,让客户端再请求一次slot 2,与MOVED
命令不同点在于,客户端接收到ASK
命令之后不会去更新自己的缓存,后续还是会先来slot 1请求