Redis 专题:一文搞懂主从复制原理

微信搜索公众号“码路印记”,感谢关注!

在分布式环境中,数据副本 (Replica) 和复制
 
(Replication) 作为提升系统可用性和读写性能的有效手段被大量应用在各种分布式系统中,Redis 也不例外。
Redis主从复制,以一主多从的模式建立的分布式系统,是Redis搭建高可用集群(哨兵模式、Cluster模式)的基础,为容错、故障转移提供强有力的支撑。
本文将介绍以下内容:

主从示例

Redis设置主从结构有三种方式:

  • replicaof  
    replicaof 192.168.1.2 6379
    192.168.1.2:6379
    replicaof
    
  • 在redis-server启动参数中增加“–replicaof ”参数。

  • 使用redis-cli客户端连接到redis服务,执行 replicaof
    命令。 注意:如果Redis重启,主从关系将无法重新建立

注意:Redis 5.0之后,replicaof已经替换slaveof,由于一些种族歧视的奇葩要求,Redis已经对指令进行了兼容升级,目前slaveof还是可以使用的。

搭建主从结构

本文示例采用docker compose搭建一主两从的主从结构。

  • 新建 docker-compose.yml
    添加以下配置信息。
version: "3"
services:
redis-master:
image: redis:6.0
ports:
- "6379:6379"
container_name: "redis-master"
command: redis-server
networks:
- redis-master-slave
redis-slave-1:
image: redis:6.0
ports:
- "6380:6379"
container_name: "redis-slave-1"
command: redis-server --slaveof redis-master 6379
depends_on:
- redis-master
networks:
- redis-master-slave
redis-slave-2:
image: redis:6.0
ports:
- "6381:6379"
container_name: "redis-slave-2"
command: redis-server --slaveof redis-master 6379
depends_on:
- redis-master
networks:
- redis-master-slave
networks:
redis-master-slave:
  • cd到当前目录,执行指令 docker-compose up
    启动docker,效果如下:
image.png
  • 客户端连接至master节点(localhost:6379)命令行执行 info replication
    查看主从关系,可以看到有两个从节点。
image.png

测试数据同步

  • 在master客户端命令行执行指令 set name abcd
    ,然后到slave节点客户端命令行执行 get name
    ,可以看到数据同步成功,如下图所示:

  • 在master客户端执行指令 del name
    ,然后到slave客户端再执行 get name
    ,可以看到数据已经删除。


由上述过程,我们可以看到, redis-master
redis-slave-1
redis-slave-2
三个节点建立起了一主二从的关系;而且,master节点写入的数据成功同步至至两个slave节点。主从关系示例先介绍到这里,接下来我们了解一下Redis主从节点之间数据同步的原理与过程。

Replication工作原理

Redis Replication是一种简单、易用的主从模式(master-slave)的复制机制,它能够使得slave节点成为与master节点完全相同的副本。每次与master节点连接中断后slave节点会自动重联,并且无论master节点发生什么,slave节点总是尝试达到与master节点一致的状态。Redis采取了一系列的辅助措施来保证数据安全。
Redis主从复制技术有两个版本:在2.8版本之前,每次slave节点断线重联后,只能进行全量同步。在2.8版本之后进行了重新设计,引入了部分同步的概念。本文将以Redis 6.0版本为基础对主从复制原理进行介绍。
旧版主从复制采用的是“全量同步+命令传播”机制完成主从数据同步,这里的硬伤是从机重连后,哪怕主从之间只有少量的数据不一致,也要执行一个耗时、耗资源的全量同步操作来达到数据一致。为此,Redis团队引入了若干机制确保在少量数据不一致时,采用代价较低的部分同步来完成主从复制。所以,当前的主从复制机制包含三个部分:全量同步、部分同步、命令传播。
为保证内容完整性,还是先介绍一下全量同步、命令传播两个历史的概念。

  • 全量同步:master节点创建全量数据的RDB快照文件,通过网络连接发送给slave节点,slave节点加载快照文件恢复数据,然后再继续发送复制过程中复制积压缓冲区内新增的命令,使之达到数据一致状态。
  • 命令传播:如果master-slave节点保持连接,master节点将持续向slave节点发送命令流,以保证master节点数据集发生的改变同样作用在slave节点数据集上,这些命令包含:客户端写请求、key过期、数据淘汰以及其他所有引起数据集变更的操作。

同步原理

在主从复制模式下,Redis使用一对 Replicaion ID, offset
来唯一识别Master节点数据集的版本,要理解这个“版本“的概念需要认识Redis的以下三个概念:

  • Replication ID(复制ID):每个Redis的主节点都用一个随机生成的字符串来表示在某一时刻其内部存储数据的状态,“某一时刻”可以理解为其成为master角色的那一刻,由源码可知在第一个从节点加入时,Redis初始化了复制ID。
  • offset(复制偏移量):主从模式下,主节点会持续不断的向从节点传播引起数据集更改的命令,offset所表示的是主节点向从节点传递命令字节总数。它不是孤立存在的,需要配合复制积压缓冲区才能工作。
  • backlog(复制积压缓冲区):它是一个环形缓冲区,用来存储主节点向从节点传递的命令,它的大小是固定的,可存储的命令有限,超出部分将会被删除。它即可用于部分同步,也可用于命令传播阶段的命令重推。

概念可能比较抽象,通过下图直观看下它们之间的关系。

图示说明:

  • 图示Redis角色为Master,其复制ID(replid)为xxxx,当前的复制偏移量(offset)为1010;
  • 它有一个复制积压缓冲区(backlog),容量(backlog_size)为100,backlog起点相对于offset的偏移量(backlog_off)为1000,当前backlog存储的命令字节数(backlog_hislen)为11个,对应了backlog中[1000,1010]偏移量范围内的字节;
  • offset始终与backlog中最后一个字节的偏移量相同。

以上内容是master节点存储维护的,在slave中也有复制ID、复制偏移量的概念。

  • 复制ID:如果slave从未与任何master进行过主从复制,那就不存在;如果slave与master连接正常,复制ID存储在 server.master
    内;如果slave与master连接断开,Redis会把它存储在 server.cache_master
  • 复制偏移量:与复制ID一致。

server.master
代表的是正在进行主从复制且正常工作的主节点信息; server.cache_master
是曾经正常进行主从复制工作的主节点信息,它是为部分同步做准备的。
扯了这么多,这些东西有什么用呢?前面也说了,旧版本每次从节点连接到主节点都要进行全量同步,效率很低,所以引入了部分同步。上面这些组件就是为了在一定条件下可以使用部分同步,提高效率。那它们是如何工作的呢?

  • PSYNC  
    server.cache_master
    PSYNC ? -1
    
  • 主节点接收到命令,解析请求中的复制ID和offset,然后去判断当前请求是否可以使用部分同步。
  • 能够使用部分同步需要满足以下两个条件( 这里先不考虑主从切换导致的多复制ID情况
    ):

    • 复制ID与主节点的复制ID一致;
    • 复制偏移量offset必须在backlog_off和offset的范围之间;
  • 不能使用部分同步,就不得不使用全量同步了。

便于理解,接下来看几个例子,如下图所示。有四个slave节点要与主节点发起主从复制,主节点、从节点的状态已经在图中标示,我们来分析下。

  • Redis-1:replid和offset为默认值,说明它从未与主节点进行过同步操作,所以是进行全量同步;
  • Redis-2:replid主从节点一致,slave_offset>=backlog_off并且slave_offset<offset,说明该从节点丢失的数据可以通过复制积压缓冲区找回,所以可以进行部分同步;
  • Redis-3:replid主从节点一致,slave_offset<backlog_off,说明该节点丢失的数据过多,通过复制积压缓冲区无法找回,所以是进行全量同步;
  • Redis-4:replid主从节点一致,之前不是与当前节点进行主从复制,所以是进行全量同步;

总结一下:部分同步其实是以全量同步为基础(得到复制ID),用复制积压缓冲区中的缓存命令做命令重放的增量同步逻辑,不过受制于复制积压缓冲区的容量,它可容忍的范围是有限的。这与持久化机制的AOF混合持久化如出一辙,也与mysql中主从复制的Binlog思路不谋而合。

命令传播

当完成同步操作之后,master-slave便会进入命令传播阶段,此时master-slave的数据是一致的。

当maste执行完新的写命令后,会通过传播程序把该命令追加至复制积压缓冲区,然后异步地发送给slave。slave接收命令并执行,同时更新slave维护的复制偏移量offset。命令传播如下图所示:
如果slave可以收到每条传播指令,并执行成功,便可以保持与master的数据一致状态。但是master并不等待slave节点的返回,master与slave是通过网络通信,由于网络抖动等因素,命令传播过程不保证slave真正接收到,那如何在传播阶段确保主从数据一致呢?

在命令传播阶段,每隔一秒,slave节点向master节点发送一次心跳信息,命令格式为 REPLCONF ACK

命令中的offset是就是slave最新的复制偏移量,master接收后便会与自己的offset对比。 如果从节点数据缺失,主节点会推送缺失的数据(这句话写的很虚,我在源码中没有找到相关逻辑,但是参考的所有文章都提到了这个点。如果哪位同学了解,烦请告知)

Replication执行流程

按照我的理解,我把主从复制的流程分为三个阶段:准备阶段、同步阶段、命令传播阶段,先上图再依次解释。下图左侧为主从复制的整体流程,右侧为新增加一台slave节点后slave的主从复制日志输出。

准备阶段

准备阶段完成与master的连接,主从之间通过命令一问一答处理状态检查及身份认证工作,为接下来的数据同步打好基础。

  • 设置master节点host和port:这一步很好理解,当接收到slaveof命令后,Redis会设置master节点的host和port,同时设置服务的复制状态为 REPL_STATE_CONNECT
  • serverCron
    REPL_STATE_CONNECT
    connectWithMaster
    REPL_STATE_CONNECTING
    syncWithMaster
    
  • 发送PING命令:与master连接创建成功后,slave向master发送PING命令。通过PING命令检查网络连接的读写状态是否正常,还可以检查master能否正常处理命令请求。如果slave读取PING命令回复超时或者master返回错误信息,将断开并重连服务器。服务器返回PONG,则继续执行下一个步骤。
  • 身份认证:检查从服务器是否设置masterauth,设置则进行身份验证,未设置则跳过该步骤。身份认证失败,则断开重连。
  • 发送slave端口信息:slave向主服务器发送自己的监听端口号,master收到之后会将端口号记录到slave对应的状态属性中,可通过命令 client list
    查看。

同步阶段

同步是主从复制最复杂的阶段,基于上面的工作原理介绍,我们知道可能为部分同步,也可能为全量同步。所以,接下来的流程就是先确定同步方式,然后再分别按照不同的方式执行同步流程。

确定同步方式

该阶段首先要做的就是master-slave共同协作确定同步的方式,简单来说就是两步:slave发送PSYNC命令、master判断决定同步方式进行回复。

slave发送PSYNC指令。PSYNC命令要为参数replid和offset赋值,这里需要区分两种情况:

  • 如果slave是第一次与当前master进行主从复制,则使用默认值: replid=?; offset=-1
  • 如果slave之前与该master进行过全量复制,则根据缓存的master信息、复制偏移量进行赋值;

怎么判断slave是第一次进行主从复制呢?Redis核心结构server的 cached_master
保存了master节点的信息,只有进行过主从复制才会赋值,否则为空。以下代码片段来自函数 slaveTryPartialResynchronization

// 代码文件:replication.c 函数:slaveTryPartialResynchronization
// 是否有缓存的master节点信息
// 有:执行if
if (server.cached_master) {
// 这里的replid,复制id就是master的replid
psync_replid = server.cached_master->replid;
// 获取复制偏移量
snprintf(psync_offset,sizeof(psync_offset),"%lld", server.cached_master->reploff+1);
serverLog(LL_NOTICE,"Trying a partial resynchronization (request %s:%s).", psync_replid, psync_offset);
} else {
// 没有:走else
serverLog(LL_NOTICE,"Partial resynchronization not possible (no cached master)");
// 设置默认值:? -1
psync_replid = "?";
memcpy(psync_offset,"-1",3);
}

/* 向master发送PSYNC命令 */
reply = sendSynchronousCommand(SYNC_CMD_WRITE,conn,"PSYNC",psync_replid,psync_offset,NULL);

master判断决定同步方式。master接收命令后由 syncCommand
函数处理,然后调用 masterTryPartialResynchronization
函数判断同步的方式,从代码分析可知,有以下几种情况:

  • offset解析失败,执行全量同步;
  • replid与master的复制ID不一致,执行全量同步;
  • offset不在master的backlog缓冲区范围内,执行全量同步;
  • 其余满足部分同步条件,执行部分同步;

接下来,从master和slave两个角度详细看下全量同步和部分同步的执行流程。

全量同步流程

先看master。确认需要执行全量同步后,master直接进入处理流程。这里提一下,全量同步过程中Redis是依靠slave的状态来驱动整个流程的,我先通过一张图描述下全量同步过程及slave的状态流转,再做说明:

  • 修改当前slave的复制状态为 SLAVE_STATE_WAIT_BGSAVE_START
    (等待bgsave操作开始)并把它加入从机列表;
  • 如果当前slave是第一个从机,需要生成新的Replication ID、清除Replication ID2、初始化复制积压缓冲区;
  • 开始执行bgsave,slave对应的bgsave流程开始后,其复制状态会修改为 SLAVE_STATE_WAIT_BGSAVE_END
    ,需要考虑以下三种情况:
    • 如果已经有子进程在执行,并且为磁盘模式:复用当前的bgsave,保存缓冲区状态;然后向slave回复全量同步,offset为正在进行bgsave启动时的复制偏移量;
    • 如果已经有子进程在进行,但是为无盘模式:暂时不执行bgsave,等待周期性检查时触发;
    • 如果没有子进程在进行:开启bgsave;然后向slave回复全量同步,offset为当前master的复制偏移量。
  • backgroundSaveDoneHandler
    updateSlavesWaitingBgsave
    SLAVE_STATE_WAIT_BGSAVE_END
    SLAVE_STATE_SEND_BULK
    SLAVE_STATE_ONLINE
    

在看slave侧。在准备阶段,slave发送psync指令后,就等待master的回复,当收到全量同步的回复后,开始执行全量同步流程。过程如下:

  • 如果slave还有级联的slave,则断开所有与它们的网络链接,并清空复制积压缓冲区;
  • 创建rdb临时文件,接收master传输的文件流并写入;
  • 停止正在进行的rdb持久化、aof持久化流程;
  • 重命名临时文件为正式的rdb文件,执行数据加载;
  • 基于当前与master的网络链接,创建slave的客户端,把master作为slave的客户端;
  • 设置slave的复制id,创建复制积压缓冲区;
  • 进入命令传播阶段;

部分同步流程

相对于全量同步,部分同步要简单的多。
master判定可以使用部分同步方式,执行以下流程:

  • 修改slave状态为 SLAVE_STATE_ONLINE
    ,并把slave加入从机列表;
  • 向slave回复部分同步命令,”+CONTINUE replid”;
  • 按照slave请求的offset,从复制积压缓冲区提取命令发送至slave;

slave接收到部分同步的回复后,执行以下流程:

  • 对比master复制ID是否发生改变,如果改变了,则更新复制ID,并把原来的复制ID转移至复制ID2;如果有级联的slave,需要断开连接,让他们重连;
  • 基于当前与master的连接,创建slave的客户端,准备接收命令。
  • 接收master传输的命令并执行;
  • 进入命令传播阶段。

命令传播阶段

与工作原理部分一致,不再重复写了。

Replication相关其他问题

从机只读

默认情况下,从机工作在只读模式下,即无法对从机执行写指令。如下图,对从机执行写指令将会返回错误。
若要更改此模式,可在配置文件修改如下选项:

# 默认是yes-只读,no-可写
slave-read-only yes/no

如果slave也配置了自己的从服务器(sub-slave),那么sub-slave只会同步从master服务器同步到slave的数据,而不会同步我们直接写入slave服务器的数据。如: A--->B--->C
,如果B关闭了只读模式,C只会同步来自A的命令且与A保持一致。

过期key处理

Redis可以通过设置key的过期时间来限制key的生存时间,Redis处理key过期有惰性删除和定期删除两种机制,这一机制依赖Redis实例的计时能力。如果主机、从机同时启用key过期的处理机制,可能会导致一些问题。为此,Redis采取了三个技术手段来解决key过期的问题:

  • 从机禁用主动key过期机制。主机在执行key过期后,会以 DEL
    指令的方式向所有从机传播指令,从而保证从机移除过期的key。
  • 依赖主机的key过期机制是无法做到实时性的,所以针对读操作,从机将会按照自己的时钟向客户端返回key不存在。
  • 为防止Lua脚本执行期间key过期,Lua脚本将会传播给从机执行。

min-replica机制

Redis主从复制不仅仅是解决主机、从机之间数据同步的问题,它还需要保证数据的安全性。这里的安全性主要是指主从之间数据同步达到一致的效率,以及主从结构下读写分离场景中分布式系统的可靠性。
Redis采用异步复制机制,它无法真正保证每个从机都能准确的收到传播的指令,所以主从之间必然会存在命令丢失的时间窗口。

为此,Redis引入了 min-replicas
选项,该机制在redis.conf中有两个配置项:

  • min-replicas-to-write :至少有N个从机才能写入数据。保证从机最低数量。

  • min-replicas-max-lag :如果每个从机的延迟值大于N,则拒绝写入数据。保证主从同步延迟。

通过 info replication
可以查看从机数量(connected_slaves)、每个从机的延迟值(lat)。
这一机制是通过从机与主机之间心跳来实现的,如上文所讲,从机每隔一秒向主机发送一次心跳数据,基于心跳,主节点可以:

  • 更新从机同步确认时间:基于主节点时间及同步确认时间计算延迟值。
  • 更新主从最后通信时间:用于从机通信超时检测,如果通信超时,主机会移除从机。

主机关闭持久化时复制的安全性

当master关闭了持久化时,如果发生故障后自动重启时,由本地没有保存持久化的数据,重启的Redis内存数据为空,而slave会自动同步master的数据,就会导致slave的数据也会被清空。
所以,我们应该尽可能为master节点开启持久化,这样可以防止Redis故障重启时数据丢失,进而导致slave数据被清除。如果确实无法开启持久化机制,那应该配置master节点无法自动重启,确保从机可以成为新的master节点,防止数据被清除。

全文总结

本文通过示例介绍主从结构搭建的方式及数据同步效果,然后详细介绍了Redis主从复制的工作原理,即“全量复制+部分复制+命令传播”,然后结合源码详细介绍主从复制的执行流程,最后介绍了一些主从复制相关的其他问题。通过这些问题,大体是介绍清楚了Redis的主从同步原理,希望能给大家带来帮助。
这篇文章差不多写了4天,中间推翻了三次,主要是想找一个比较清晰的思路来把这个问题讲述清楚。能力太差,还是需要多练。
如果觉得有用,请转发给更多需要的朋友,感谢您的阅读!码字不易,感谢支持!

参考文献

  • 《Replication》:https://redis.io/topics/replication
  • Redis 复制过程详解
    》: https://mp.weixin.qq.com/s/0VVYTyAI1egfs2Fxcrme3A

  • 《redis系列:主从复制》:https://juejin.cn/post/6844903650175746056#heading-15
  • 《Redis 主从复制技术原理》:https://juejin.cn/post/6844904106331488264
  • 《关于Redis的主从复制》:https://juejin.cn/post/6854573214203871245#heading-12
  • 《Redis高可用——主从复制》:https://juejin.cn/post/6844904098773352455#heading-8
  • 《Redis 主从复制 步骤详解》:https://juejin.cn/post/6927186654356570125#heading-21
  • 《Redis主从复制机制详解》:https://juejin.cn/post/6844903880745025543
  • 《Redis 源码分析之主从复制(4)》:https://segmentfault.com/a/1190000020984164
image.png