Redis作为分布式缓存,必然要保证自身的高可用。而分布式系统实现高可用的方式主要就是主从和集群。Redis也不例外,它有自己的Master节点和Slave节点。Redis既支持传统的数据集中集群模式,也支持数据分散集群模式。本章,我们先来看看Redis的数据集中集群模式,也就是Master/Slave模式。
对分布式理论还不是很清楚的童鞋,可以回头去看看基础篇的集群章节。
在主从架构下,Redis的Master节点和Slave节点都各自保存着完整的数据,Slave节点会从Master节点同步数据:
当启动一个slave node时,它会发送一个PSYNC
命令给master node。psync
命令用于完成主从数据同步,数据同步可能有两种情况: 全量复制 和 增量复制 。
一、全量复制
如果这个slave node是第一次连接master node,那么会触发一次全量复制(full resynchronization)。全量复制的整体流程如下:
- master node收到
psync
命令后执行bgsave
命令,在后台生成RDB文件,同时将从现在开始执行的所有写命令记录到缓冲区; - RDB文件生成完毕后,master node将其发送给slave node。slave node接收保存,然后载入这个RDB文件;
- master node将缓冲区中的命令发送给slave node,slave node将自己的状态更新至Master当前所处的状态,保持数据最终一致。
二、增量复制
增量复制,主要用于处理在主从复制过程中,因 网络闪断 等原因造成的数据丢失场景。如果全量复制过程中,master-slave网络连接断掉,那么salve重新连接master时,可能会触发增量复制。
此时,master直接从自己的backlog中获取部分丢失的数据,发送给slave node,默认backlog就是1MB。因为补发的数据远远小于全量数据,所以可以有效避免全量复制的开销。
*那么问题来了,slave如何知道要从哪个地方开始获取数据呢?*从Redis 2.8开始,Redis开始支持主从复制的断点续传,整个过程如下:
看不懂?没关系,我来详细讲解下,部分复制的核心包含三部分:
- master node的复制偏移量( replication offset )和slave node的复制偏移量;
- master node的复制积压缓存区( replication backlog );
- 服务器的运行ID( run ID )
2.1 replication offset
复制偏移量(replication offset),标识着master和slave的数据同步进度。
当slave因为网络闪断等原因与master失去连接后,如果再次连接上master,会通过PSYNC
命令将自己的offset发送给master,master根据偏移量决定执行全量复制还是部分复制:
- 如果offset之后的数据(offset+1开始)仍存在于master的复制积压缓存区中,则进行部分复制;
- 否则,进行全量复制。
当master每次向slave传播N个字节的数据时,就将自己的offset值加上N;slave每次收到master传播来的N个字节数据时,就将自己的offset值加上N:
2.2 replication backlog
复制积压缓存区,是由master维护的一个固定大小的FIFO队列(默认为1MB),主要是用来做全量复制中断后的增量复制。当master进行命令传播(给slave发送数据)时,它不仅会将写命令发送给所有salve,还会将写命令入队到复制积压缓冲区里面。也就是说,复制积压缓存区保存着一部分最近传播的写命令:
当主从节点网络中断后,slave再次连上master时,会发送
psync{offset}{runId}
命令请求部分复制。如果请求的offset不在master的积压缓冲区内,则无法提供给slave数据,因此部分复制会退化为全量复制。
2.3 run ID
每个Redis服务节点,在启动时都会生成自己的run ID。run ID由40个随机的十六进制字符串组成。当进行初次全量复制时,master会将自己的运行ID传送给slave,而slave则将这个ID保存起来。当slave断线重连成功后,会将之前保存的运行ID发送给master:
- 如果Master发现Slave上送的运行ID与自身的运行ID不同,则进行全量复制;
- 如果Master发现Slave上送的运行ID与自身的运行ID相同,则进行部分复制。
如果master因故障重启,那么它的run ID会改变,slave发现与master的运行ID不匹配时,会认为自己复制的是一个新的master节点,从而进行全量复制。这种情况应从架构上规避,比如提供 故障转移功能 :当主节点发生故障后, 自动提升从节点为主节点 。
三、数据一致性问题
Redis主从架构在进行数据复制的时候,可能会有数据丢失,导致出现数据一致性问题,主要发生在两种场景下:复制延迟、脑裂。
3.1 复制延迟
由于主从之间的数据复制是异步的,所以当Client往Master节点写入数据后,Master会立即响应Client结果,然后异步往从节点同步数据。那么主从节点在某一时刻,数据可能是不一致的,如果网络延迟比较大,那主从节点间的数据相差可能会比较多。
比如上图中,主节点的offset是10119,从节点是10000,那此时主从节点之间的数据是不一致的,如果此时Master突然宕机,然后进行了故障自动转移,Slave晋升为新的Master节点,那旧Master中的部分数据就丢失了。最重要的是,这种延迟是不可控的,如果网络经常出现抖动,主从之间的数据差异会越来越大,那当Master节点宕机时,丢失的数据就会很多。
3.2 脑裂
脑裂,也就是网络分区,即某个master突然脱离了正常的网络,跟其他slave不能连接了,但是实际上master还运行着。此时哨兵可能就会认为master宕机了,将其它slave切换成master,这个时候,集群里就会有两个master,也就是所谓的脑裂。
此时,Client可能还没来得及切换到新的master,继续向旧master写数据。当网络分区恢复后,旧master会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据,这样就发生了数据丢失。
3.3 解决方案
我们可以通过设置一些Redis参数来解决上述这两个问题:
min-slaves-to-write
:要求至少有多少个slave;min-slaves-max-lag
:数据复制和同步的最大延迟(秒)。
上面的参数的含义是:
如果Slave节点的数量少于 min-slaves-to-write 个,或所有Slave节点的复制响应延迟都超过10秒,那么Master就直接拒绝客户端的写请求 。
min-slaves-to-write
可以用来解决脑裂问题,比如在一个1主2从的架构下,我们将min-slaves-to-write
设置为1,那么当出现网络分区时,如果主节点被单独隔离,由于此时它没有了从节点,所以会拒绝客户端的写入,从而保证了数据一致性。
min-slaves-max-lag
可以用来解决复制延迟问题,比如min-slaves-max-lag
设置为10。那么当Master接受到客户端的写请求后,如果有一个slave的ack延时超过10秒,那Master就会认为与Slave的数据差异过大,就拒绝写请求。这样就可以将复制延迟导致的可能导致的数据丢失控制在可控范围内,比如按照上面的配置,最多就丢失10秒的数据。
当Client往Master写入数据失败后,其实可以将数据临时写入到磁盘或消息队列(相当于针对缓存失效的降级方案),然后由异步任务定时取磁盘或队列数据重新发送到Master。
四、总结
Redis实现高可用的最基本方式就是构建主从架构,主从节点之前会进行数据同步,以保证数据的一致性。主从之间的数据同步方式有 全量复制 和 增量复制 。