本章将介绍2.8以前的老版复制功能和2.8以后的新版复制功能,讲解机制和优劣势。

在Redis中,用户可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制另一个服务器,我们称呼被复制的服务器为主服务器(master),而对主服务器进行复制的服务器则被称为从服务器(slave)。搞清楚关系,如果服务器A输入指令SLAVEOF,则A变成B的从服务器。

进行复制中的主从服务器双方的数据库将保存相同的数据,概念上将这种现象称作“数据库状态一致”,或者简称“一致”。比如,在服务器上执行命令,

127.0.0.1:6379> SET msg "hello world"
OK

则同时可以在服务器上获取msg键的值,

127.0.0.1:12345> GET msg
"hello world"

1. 旧版复制

1.1 旧版复制的实现

Redis的复制功能分为同步(sync)命令传播(commandpropagate)两个操作:

  • 同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
  • 命令传播操作则用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,让主从服务器的数据库重新回到一致状态。

(1)同步

从服务器对主服务器的同步操作需要通过向主服务器发送SYNC命令来完成,以下是SYNC命令的执行步骤:

  1. 从服务器向主服务器发送SYNC命令
  2. 主服务器收到后,执行BGSAVE命令,生成RDB文件,并使用缓冲区记录现在开始执行的所有写命令。
  3. 将RDB文件发送给从服务器,从服务器收到后更新
  4. 主服务器将缓冲区的内容发送给从服务器,从服务器收到后更新。

BGSAVE命令会增加一个子进程,负责创建RDB文件。

(2)命令传播

主服务器会将自己执行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器执行,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态。

1.2 旧版复制的缺陷

在Redis中,从服务器对主服务器的复制可以分为以下两种情况:

  • 初次复制:从服务器以前没有复制过任何主服务器,或者要复制的主服务器和上一次复制的主服务器不同。
  • 断线后重复制:处于命令传播阶段的主从服务器因为网络原因而中断了复制,但从服务器通过自动重连接重新连上了主服务器,并继续复制主服务器。

初次复制效果挺好的,但断线后重新复制效率就很低。因为执行SYNC命令是非常消耗资源的行为。

2. 新版复制

2.1 新版复制功能的实现

Redis从2.8版本开始,使用PSYNC命令代替SYNC命令来执行复制时的同步操作。

PSYNC命令具有完整重同步(full resynchronization)部分重同步(partial resynchronization)两种模式:

  • 完整重同步用于初次复制,和SYNC命令完全一致
  • 部分重同步,将断线后的命令发送给从服务器。

要实现部分重同步,需要完成三个部分:

  • 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量。
  • 主服务器的复制积压缓冲区(replication backlog)。
  • 服务器的运行ID(run ID)。

(1)复制偏移量

主服务器和从服务器会分别维护一个复制偏移量:

  • 主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。
  • 从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值加上N。

通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态

  • 如果主从服务器处于一致状态,那么主从服务器两者的偏移量总是相同的。
  • 相反,如果主从服务器两者的偏移量并不相同,那么说明主从服务器并未处于一致状态。

(2)复制积压缓冲区

复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认大小为1MB。当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里面

与此同时,主服务器也会向积压缓冲区添加偏移量,

当服务器重新连接上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作

  • offset偏移量之后的数据仍然存在于复制积压缓冲区中,主服务器执行部分重同步操作
  • 反之,偏移量之后的数据已不存在于复制积压缓冲区,则执行完整重同步。

复制积压缓冲区作为一个限制性容器保证了复制的高效性:

  • 如果断线时间短,错过的命令少,则直接调用偏移量为从服务器补上命令
  • 反之,则直接完全重同步。

(3)服务器运行ID

每个Redis服务器,不论主服务器还是从服务,都会有自己的运行ID,运行ID在服务器启动时自动生成,由40个随机的十六进制字符组成。

当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器则会将这个运行ID保存起来。

当从服务器断线并重新连上一个主服务器时,从服务器将向当前连接的主服务器发送之前保存的运行ID:

  • 如果ID相同,则表示之前同步的主服务器就是这个,执行部分重同步。
  • 如果ID不同,则表明从服务器断线之前复制的主服务器并不是当前连接的这个主服务器,执行完整重同步操作。

2.2 PSYNC命令的实现

PSYNC命令的调用方法有两种:

  • 如果是初次复制,则从服务器发送PSYNC?-1命令,主动请求主服务器进行完整重同步。
  • 如果已经复制过,则从服务器发送PSYNC<runid><offset>命令。即:上一次复制的主服务器ID+当前的复制偏移量。

根据情况,接收到PSYNC命令的主服务器会向从服务器返回以下三种回复的其中一种:

(1)如果主服务器返回+FULLRESYNC <runid> <offset>回复,那么表示主服务器将与从服务器执行完整重同步操作。从服务器将ID保存起来,在下一次PSYNC命令时使用,同时将offset的值当做自己的初始化偏移量。

(2)如果主服务器返回+CONTINUE回复,那么表示主服务器将与从服务器执行部分重同步操作,从服务器只要等着主服务器将自己缺少的那部分数据发送过来就可以了。

(3)如果主服务器返回-ERR回复,那么表示主服务器的版本低于Redis2.8,它识别不了PSYNC命令,从服务器将向主服务器发送SYNC命令,并与主服务器执行完整同步操作。

2.3 新版复制的完整流程

本节主要展示新版复制操作的全过程,假设主服务器IP地址为127.0.0.1端口号为6379,从服务器IP为127.0.0.1端口号12345.

(1)设置主服务器地址和端口

客户端从服务器发送以下命令时:

127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
OK

从服务器首先要做的就是将客户端给定的主服务器IP地址127.0.0.1以及端口6379保存到服务器状态的masterhost属性和masterport属性里面:

struct redisServer{
    //...
    char *masterhost;
    int masterport;
    //...
};

(2)建立套接字连接

从服务器将根据命令所设置的IP地址和端口,创建连向主服务器的套接字连接

此时,从服务器变为了主服务器的客户端。从服务器同时具备服务器和客户端的两个身份。

(3)发送PING命令

连接成功后,从服务器立马发送一个PING命令,主要作用是:

  • 检查套接字读写是否正常
  • 检查主服务器能否正常处理命令

回复有三种可能:

  • 主服务器返回了命令回复,但从服务器不能再规定的时间内读出,表明主从之间网络连接不佳。从服务器断开并重新创建连向主服务器的套接字。
  • 主服务器返回一个错误,表示主服务器暂时无法处理请求(比如正在处理一个超时运行脚本),从服务器断开并重新创建连向主服务器的套接字。
  • 从服务器收到PONG回复,表示主从之间连接正常。

(4)身份验证

收到pong的回复后,下一步是确定是否进行身份验证:如果从服务器设置了masterauth选项,那么进行身份验证;反之则不进行。

(5)发送端口信息

从服务器向主服务器发送从服务器的监听端口号。主服务器在接收到这个命令之后,会将端口号记录在从服务器所对应的客户端状态的slave_listening_port属性中:

typedef struct redisClient 
{ 
    // ... 
    // 从服务器的监听端口号 
    int slave_listening_port; 
    // ...
} redisClient;

(6)同步

在这一步,从服务器将向主服务器发送PSYNC命令,执行同步操作,并将自己的数据库更新至主服务器数据库当前所处的状态。

在同步操作执行之前,只有从服务器是主服务器的客户端,但是在执行同步操作之后,主服务器也会成为从服务器的客户端

(7)命令传播

主服务器只要一直将自己执行的写命令发送给从服务器,而从服务器只要一直接收并执行主服务器发来的写命令,就可以保证主从服务器一直保持一致了。

4. 心跳检测

命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:

REPLCONF ACK <replication_offset>

其中replication_offset是从服务器当前的复制偏移量。发送REPLCONF ACK命令对于主从服务器有三个作用:

  • 检测主从服务器的网络连接状态。
  • 辅助实现min-slaves选项。
  • 检测命令丢失。

(1)检测连接状态

如果主服务器超过一秒钟没有收到从服务器发来的REPLCONF ACK命令,那么主服务器就知道主从服务器之间的连接出现问题了。

(2)辅助实现min-slaves选项

Redis的min-slaves-to-writemin-slaves-max-lag两个选项可以防止主服务器在不安全的情况下执行写命令。

举个例子,如果我们向主服务器提供以下设置:

min-slaves-to-write 3
min-slaves-max-lag 10

那么在从服务器的数量少于3个,或者三个从服务器的延迟(lag)值都大于或等于10秒时,主服务器将拒绝执行写命令。

(3)检测命令丢失

假如主服务器的向从服务器发送的传播命令因为网络问题丢失,会导致二者偏移量不一致。这是心跳检测命令会侦察到这种情况,于是主服务器会补发。