《Redis设计与实现》主从复制
本文最后更新于 2025年8月29日 晚上
主从复制
用户可以通过执行SLAVEOF命令或者设置slaveof选项,来让一个服务器去复制另一个服务器。此时,我们称被复制的服务器为主服务器,复制其他服务器的服务器为从服务器。比如,两个服务器地址分别为127.0.0.1:6379和127.0.0.1:12345,如果用户在服务器127.0.0.1:12345中发送命令:
1 | |
那么服务器127.0.0.1:12345将成为127.0.0.1"6379的从服务器。在此之后,主从服务器中将保存相同的数据。
旧版复制功能
在发出SLAVEOF指令之后,从服务器首先需要执行同步操作。具体流程如下
sequenceDiagram
participant B as 从服务器
participant A as 主服务器
participant SubA as BGSAVE子进程
participant C as 客户端(用户)
B ->> A: 发送Sync命令
A ->> SubA: 执行BGSAVE命令
Note right of A: 生成RDB快照文件
C -->> A: 新的写入命令
A -->> A: 将写入命令记录到缓冲区
destroy SubA
SubA ->> A: BGSAVE命令执行完毕
A ->> B: 发送RDB文件
B ->> B: 接受并载入RDB文件
A ->> B: 发送缓冲区保存的所有写命令
B ->> B: 接受并执行主服务器发来的所有写命令
数据传播
从服务器同步完主服务的数据后也并不代表着结束。后续有客户端给主服务发送写命令时,主服务器中的数据也会和从服务器不一致。所以我们还需要一个方法将这种变化也同步到从服务器中去。Redis中采用的方法为,**当主服务器收到新的写指令时,在它操作完自己的数据之后,会将这个写指令发送给它的所有从服务器。**以此,达成数据同步的效果。
sequenceDiagram
participant C as 客户端(用户)
participant A as 主服务器
participant B as 从服务器
C ->> A: 发送写指令
A ->> A: 执行写指令
A ->> B: 发送写指令
B ->> B: 执行写指令
这是书上给的解释,但是这个解释看起来很奇怪,Redis中,主服务器真的会每完成一个写操作之后都会将其立马发送给从服务器吗?
在网上搜了一下解释之后,我得到了这个结论:不会!
事实上,主服务器在执行完写指令之后,会现将这个指令写入复制积压缓冲区(Replication Backlog),这个知识点在后面也会提到,但是为了避免误解,这里先小提一嘴。主服务器会在之后通过这个缓冲区,异步地将命令发送给所有从服务器。
注意: 每一个已连接的从服务器,主服务器都会为其创建一个复制输出缓冲区。主服务器会将复制积压缓冲区中的新命令,分发到每个对应的复制输出缓冲区中。
复制输出缓冲区的信息发送时机,取决于操作系统的通知时机。当主服务器发现某个从服务器的Socket可写时,便会理科捕获该事件,然后立马尽最大努力发送尽可能多的指令。
旧版复制功能的缺陷
主要的缺陷体现在从服务器宕机重连的情况。在Redis中,主从复制可被分为以下两种情况:
- 初次复制:从服务器以前没有父之过任何主服务器,或者从服务器这次要复制的主服务器对象与上一次不同
- 能正常且很好地使用我们之前讲的方法进行复制
- 断线后重新复制:处于命令传播阶段的从服务器因为某种原因断线了,现在重新连接又连上了主服务器
- 也能正常使用我们之前将的方法进行复制,但是为有些问题:断线后的重连所带来的重新复制,用的是我们之前讲的相同的方法,这可能会带来很严重的性能问题,因为SYNC操作是一个非常耗费资源的操作,会涉及到主服务器大量的I/O操作与运算资源,网络资源,和从服务器的I/O资源。
旧版复制功能的问题,书中给了一个很明确的例子:

可以看到,从服务器从断开连接到重新连接中间,主服务器值增加了3个新的写命令。但是从服务器恢复的流程却是重新读取包含了整个主服务器数据库数据的RDB文件,这通厂会带来极大的资源消耗。因此,新版的断线重连复制功能不再需要主服务器写RDB文件后再让从服务器读,而是让主服务器将在从服务器断线期间执行的写命令发送给从服务器,以节省大量的资源。
新版复制功能
新版Redis中不再采用简单的SYNC机制,而是改为了PSYNC机制,而在这个机制中,在两种不同情况中会采用不同的复制流程:
- 初次复制:与原本的
SYNC机制相同 - 断线后重新复制:让主服务器将在从服务器断线期间执行的写命令发送给从服务器,从服务器接收并执行这些命令即可。
书中也给了一个很明确的例子(网上的图片有问题,这里只给一个流程图):
sequenceDiagram
autonumber
participant C as 客户端(用户)
participant A as 主服务器
participant B as 从服务器
loop 命令传播阶段
C ->> A: 写命令
A ->> A: 执行命令
A ->> B: 写命令
B ->> B: 执行命令
end
B --x B: 断线了
loop 正常接收写命令
C ->> A: 写命令
A ->> A: 执行命令
end
B -->> B: 恢复连接了
B ->> A: 发送PSYNC命令
A ->> B: 发送+Continue或<br/>+FULLRESYNC回复
A ->> B: 发送主从服务器断线期间<br/>主服务器执行的命令
B ->> B: 接收命令并执行
其中,9-11的部分便表达了部分重同步的基本流程。接下来讲一下具体流程
部分重同步
有以下三个部分组成:
- 主服务器的复制偏移量 (replication offset)和从服务器的复制偏移量
- 主服务器的复制积压缓冲区
- 服务器的运行ID
复制偏移量
执行复制的双方都会分别维护一个复制偏移量:
- 主服务器每次向从服务器传播N个字节的数据时,就会将自己的复制偏移量加上N
- 从服务器每次收到主服务器传播来的N个字节数据时,就激昂自己的复制偏移量加上N
通过对比主从服务器的复制偏移量,即可很容易地知道主从服务器是否处于同一个状态:
- 状态一致,则两者的偏移量总是相同的
- 状态不一致,则必然不相同
复制积压缓冲区
我们之前也提到过这个内容,这里便会将一些细节进行说明。
复制积压缓冲区是由主服务器维护的一个固定长度,先进先出的队列,默认大小为1MB。当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制挤压缓冲区里面。
当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作:
- 如果offset+1开始的数据还存在于复制积压缓冲区中,那么主服务器对从服务器进行部分重同步操作
- 如果不存在,则进行完整重同步操作
从这里可以发现,如果复制积压缓冲区的空间设置得太小,该方法会很容易变成单纯的完整重同步操作。因此,Redis也提供了一个方法来修改这个值。我们可以在服务器配置文件中的repl-backlong-size条目找到对应的方法,以根据需要,来调整这个值。通常来说,我们可以用这个公式来估算:
- :从服务器断线重连所需的平均时间
- :主服务器平均每秒产生的写命令数据量(字节数和)
为了安全考虑,你甚至可以在这个公式前面乘个2或是什么别的合理的数,以确保大部分断线情况都可以用部分重同步来处理。
服务器运行ID
每个Redis服务器都会有自己的运行ID,这个ID在服务器启动时自动生成。由40个随机的十六进制字符组成。
当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器则会保存这个ID。每当从服务器断线重连上一个主服务器时,从服务器都会将自己保存的这个ID发送给主服务器:
- 如果主服务器接收到的ID与自己的运行ID相同,则代表这个从服务器之前连的就是自己这个服务器。之后,才会继续尝试重同步操作。
- 如果不相同,则会进行完整重同步操作。
随机生成ID,如果出现ID重复的情况该怎么办?
- 首先,我们来看看一共有多少可能的ID:
- 然后,来算算遇到相同ID的概率:
- 也许真实计算的方法其实并不是这样的,但是也大差不差。我觉得看到这个数字就知道为什么了,这不是我们需要担心的事情。
PSYNC命令的实现
由于我很少看见有谁会问这个问题,因此我在这里就简单写一下几个可能的流程:
sequenceDiagram
participant A as 主服务器
participant B as 从服务器
participant C as 客户端
C ->> B: 发送SLAVEOF命令
alt 从服务器以前没有复制过任何主服务器<br/>或者之前执行过SALVEOF no one命令
B ->> A: PSYNC ? -1
else 从服务器已经复制过某个服务器
B ->> A: PSYNC <runid> <offset>
end
alt 主服务器执行完整冲同步操作
A ->> B: +FULLRESYNC <runid> <offset>
B ->> B: 保存 <runid>
else 主服务器将与从服务器执行部分重同步
A ->> B: +CONTINUE
A ->> B: 断线期间的命令
else 主服务器版本低于2.8
A ->> B: -ERR
B ->> A: SYNC
A ->> B: 执行旧版本的复制操作
end
复制的实现
复制的实现过程会涉及到网络连接,验证连接,身份验证,建立监听,同步操作,命令传播等一连串操作。流程比较复杂,下面给一个大致的说明:
- 从服务器将主服务器IP地址和端口号保存袋服务器状态的
masterhost属性和masterport属性中 - 主从服务器建立套接字连接
- 从服务器向主服务器请求建立套接字后,主服务器为该套接字创建客户端状态。
- 从服务器这时候将同时拥有服务器和客户端两个身份。
- 主从复制的步骤都是以从服务器向主服务器发送命令请求的形式来进行,所以从服务器的客户端身份十分重要。
- 从服务器向主服务器发送PING命令。
- 做这一步的目的有:
- 检查套接字读写状态是否正常
- 检查主服务器是否能正常处理命令请求
- 从服务器发送PING命令后可能会遇到如下情况:
- 主服务器返回了命令回复,但从服务器无法及时读取命令回复的内容。此时从服务器会断开并重创建连向主服务器的套接字
- 主服务器返回了一个错误,从服务器也会断开并重连
- 主服务器返回了命令回复,从服务器读取到了PONG命令,表示连接正常。这种情况下可以继续执行复制操作。
- 做这一步的目的有:
- 执行身份验证操作,不过这取决于从服务器是否设置了
masterauth选项- 只有当主服务器设置了
requirepass选项并且与从服务器发送的密码相同,或者主服务器没有设置requirepass选项且从服务器也没有设置masterauth选项时,才会视为身份验证通过 - 身份验证不通过的情况下,基于主从服务器的设置,决定主服务器返回的内容
- 设置了
requirepass选项但没有设置masterauth选项:返回NOAUTH错误 - 没有设置
requirepass选项但设置了masterauth选项:返回no password is set错误 - 主从服务器设置的密码不同:返回
invalid password错误
- 设置了
- 只有当主服务器设置了
- 从服务器发送端口信息。主服务器在接受到信息后,会将从服务器的端口号记录在所对应客户端状态的
slave_listening_port属性中 - 同步:就,同步
- 命令传播:就,命令传播
心跳检测
命令传播阶段,从服务器默认会议每秒一次的频率,像主服务器发送命令:
1 | |
其中replication_pffset是从服务器当前的复制偏移量。
该命令有三个作用:
- 检测主从服务器的网络连接状态
- 如果主服务器超过一秒(默认)没有收到从服务器发来的命令,则就可以认为主从服务之间的连接出了问题
- 在主服务器中发送
INFO replication命令可以查看具体信息
- 辅助实现
min-slaves选项- Redis的
min-slave-to-write和min-slaves-max-lag两个选项可有用于防止主服务器在不安全的情况下执行写命令:- 一般本地测试的时候不用管这个问题,毕竟你不用从服务器
- 前者的意思是最小需要有的从服务器数量,后者的意思是最小从服务器数量的延迟值都大于或等于n秒
- Redis的
- 检测命令丢失
- 如果主服务器在传播命令时因为网络原因导致从服务器没有接收到,导致从服务器的复制偏移量没有发生改变。这时发送这个命令便可以让主服务器发现从服务器的数据与自己的不一致,因此会再次向从服务器传播未发送的命令。