☰
Redis主从复制全解析:全量同步、增量同步与PSYNC协议实践
2026/10/23 8:20:22 网站建设 项目流程

1. 先想明白:主从模式到底是在解决什么问题

有一回我和同事排查一个线上告警,从节点断了几十秒,重连之后主节点的 CPU 和带宽突然一起飙升,日志里反复出现全量数据同步的记录。当时第一反应是网络出了问题,但查来查去,根子其实出在复制积压缓冲区太小,断线期间的增量数据已经保不住了,所以被迫退化成一次完整的全量数据同步。那次之后我越来越觉得,Redis 主从模式不难搭,但要把全量数据同步和增量数据同步这两套机制吃透,才谈得上在故障面前不手忙脚乱。

主从模式是 Redis 高可用的地基。你可以先把它理解成一个“一个老师带多个学生”的结构:主节点负责接收写请求,从节点实时跟着主节点的数据走,平时主要承担读请求。这样做最直接的好处有三个:

  • 读写分离:把读流量分散到多个从节点上,主节点压力变小。
  • 数据冗余:主节点挂了,从节点手里还有一份完整数据,能顶上。
  • 高可用基础:哨兵模式、集群模式,底层都依赖主从复制这套数据同步机制。

搭建主从的方式也很简单,无非两种。一种是配置文件中写好replicaof指令,另一种是在命令行动态执行:

# 在从节点上执行,把当前实例变成 master 的从节点 REPLICAOF 10.0.0.11 6379

注意命令是REPLICAOF,不是老写法SLAVEOF。新版本里虽然SLAVEOF还能用,但官方已经不推荐了,主从这两个词在 Redis 的运维语义里正逐步被替换掉。

很多初学者会有个疑问:主从复制到底是推数据还是拉数据?答案是“拉”。从节点主动找主节点要数据,主节点被动响应。这个设计很关键,因为如果是主节点主动推送,主节点还得维护一张从节点清单,谁掉线了、谁新加入,管理起来非常麻烦。改成从节点主动拉取之后,主节点只需要在收到请求时干活,整个链路的控制权就交给了从节点。

另一个容易误解的点是:主从模式并不保证强一致性。Redis 的复制默认是异步的,主节点执行完写命令后就返回客户端了,不会等着所有从节点都确认写完。所以从节点上的数据一定比主节点“慢一点点”,这个滞后是常态。理解了这一点,你再去看全量同步和增量同步的设计,就会有更清晰的主线:一切努力都是为了缩小这个滞后窗口,同时让断线重连后的追平成本尽可能低。

2. 全量同步:第一次握手是怎么把整份数据搬过去的

2.1 初次建立复制的入口:PSYNC ? -1

不管是从零搭建一主一从,还是从节点数据已经被污染需要重建,第一次建立复制时从节点对主节点说的一句话永远是:

PSYNC ? -1

翻译成人话就是:“我对自己要复制的这段数据历史一无所知,从哪个偏移量开始追我也不知道,干脆把我需要的所有数据都发给我吧。”主节点收到这个请求之后,如果同意全量同步,会回复:

+FULLRESYNC <replid> <offset>

replid是主节点当前复制流的身份标识,你可以把它理解成这部“数据连续剧”的剧目编号;offset是主节点当前已经写到的字节位置,相当于播放进度。拿到这两个信息之后,从节点才正式开始全量同步。

这里有个值得拆解的细节:为什么第一次同步必须用全量,而不是把主节点上的每条写命令都重新发给从节点执行一遍?原因很朴素——主节点可能已经跑了几个月,写操作记录太多了,如果从头开始逐条“补作业”,网络开销和耗时都不可控。而 RDB 快照把当前所有数据压缩成一个文件,一次性发过去,虽然过程重,但终点明确。

2.2 全量同步的完整时序:BGSAVE、积压缓冲区、清空装载

全量同步不是主节点一个BGSAVE然后把文件丢给从节点就完事儿的,中间藏着好几个容易忽略的环节。我把完整流程拆开来看:

  1. 从节点发送PSYNC ? -1,主节点决定走全量同步,返回FULLRESYNC。
  2. 主节点执行BGSAVE生成 RDB 快照文件。
  3. 在BGSAVE执行期间,主节点依然对外服务,所有新的写命令会被同时记录到复制积压缓冲区(replication backlog)里。
  4. RDB 文件生成完毕,主节点把文件发给从节点。
  5. 从节点收到 RDB 之前,会先把自己的旧数据清空,然后加载 RDB 文件。
  6. RDB 发送结束后,主节点再把积压缓冲区里这段时间积攒的写命令补发给从节点。
  7. 从节点执行完这些补发的命令,主从数据追平,进入后续的增量同步阶段。

注意第 5 步:“清空旧数据”不是在拿到 RDB 之前就做的。从节点会先把旧数据留着,等 RDB 文件完整接收且校验通过后再清空再加载,这样即使 RDB 传坏了,至少从节点还能保留原来的数据,不至于两边都变成空壳。

这里就是全量同步最容易出性能问题的环节了。主节点执行BGSAVE需要 fork 出一个子进程来生成快照。fork 本身很快,但如果实例里堆积了大量大 key,子进程在生成 RDB 时会触发内存页的复制(Copy-On-Write),内存和 CPU 都会有一波明显消耗。我在生产环境里见过上百 GB 的实例做全量同步,主节点的瞬时 CPU 直接飙到多核打满,耗时是以分钟计的。

所以 Redis 提供了一些调节阀,比如:

# 开启无盘复制,主节点不生成 RDB 文件落盘,通过 socket 直接发给从节点 config set repl-diskless-sync yes # 等待多个从节点一起请求,延迟几秒后只生成一次 RDB,服务所有从节点 config set repl-diskless-sync-delay 5

无盘复制适合主节点磁盘 IO 比较吃紧、但网卡带宽还宽裕的场景。如果从节点很多,我会建议开启repl-diskless-sync-delay,让多个从节点集合到一起,共享一次 RDB 生成结果,避免每个从节点各 fork 一次。

2.3 从节点装载 RDB 时是阻塞的,别忽视

从节点加载 RDB 文件的过程是阻塞的。也就是说,加载期间从节点没法对外提供读服务,INFO replication里会看到master_link_status:down,同时在日志里出现SYNC with master in progress。这个状态持续多久,取决于 RDB 文件的大小和机器的磁盘速度。

如果你的业务对从节点的读可用性要求比较高,可以关注一下 Redis 7 引入的repl-diskless-load配置:

# 可选:on-empty-db / swapdb,降低装载 RDB 期间的阻塞影响 config set repl-diskless-load on-empty-db

这个配置的大体思路是,从节点先把新 RDB 数据加载到临时内存,再切换视图,让旧数据尽可能多服务一会儿。不过它也有自己的副作用,比如临时内存消耗会翻倍。所以要不要开,还得看你的内存余量,不能照单全收。

再补一个容易踩的坑:如果从节点配置了 AOF 持久化,那么从节点在完成 RDB 装载之后,还会基于新数据集做一次 AOF 重写。换句话说,磁盘上会先出现短暂的“旧 AOF 和文件新内容并存”的状态,如果你监控的是磁盘空间,这段时间要做好心理准备,别把磁盘告警阈值卡得太死。

3. 增量同步:复制偏移量、积压缓冲区与命令传播

3.1 复制的“里程表”:offset 是怎么记录进度的

全量同步结束之后,主从之间就进入增量数据同步阶段,也叫命令传播阶段。这时候机制变得轻量很多:主节点每执行一条写命令,除了写自己的内存数据,还会把这条命令追加到复制积压缓冲区,同时把自身的master_repl_offset往后推进。从节点每收到一条命令并执行完,也会把自己这边的slave_repl_offset往前推进。

也就是说,主节点和从节点各自维护着一个“进度条”。虽然它们都是朝着同一个方向前进,但异步复制决定了这两个进度条不可能完全同步,一定存在一个差值,这个差值就是我前面提到的数据滞后量。

你可以把复制流想象成一条两端不断增长的长河,offset就是河上的里程标。主节点在上游,从节点在下游,河水每流一米,两边的里程标都会更新。这里的“河水”就是命令字节流,每个字节都有自己唯一的偏移量。

命令传播阶段其实就两件事:

  • 主节点往“河”里注入新的命令流,并记录master_repl_offset。
  • 从节点从“河”里消费命令流,执行后上报自己的slave_repl_offset。

从节点会每秒上报一次自己的offset,通过REPLCONF ACK <offset>命令告诉主节点“我追到哪儿了”。主节点收到之后,不但能算出滞后量,还能判断这个从节点是否还活着——如果超过timeout时间没收到 ACK,主节点就会认为这个从节点已经断开了。

3.2 断线重连后,主节点怎么决定从哪接着传

从节点断线之后,重连时发送的就不再是PSYNC ? -1了,而是:

PSYNC <replid> <offset>

意思变成:“我带着上次的身份和进度来了,你看看能不能从断点接着给我传。”

主节点收到这个请求后,会做两个判断:第一,这个replid是不是我当前的身份;第二,这个offset是否还落在我的复制积压缓冲区里。

如果两个条件都满足,主节点返回:

+CONTINUE

然后从offset对应的位置开始,把后面的命令流继续发给从节点。这个过程就是增量数据同步,也就是官方文档里说的“部分重同步”(partial resynchronization)。代价非常小,网络开销只等于断线期间积攒的那部分命令。

如果条件不满足,主节点返回FULLRESYNC,整个流程退回全量同步。具体来说,以下两种情况最常见:

  • 从节点发送的replid和主节点当前replid对不上。
  • 从节点发送的offset太旧了,已经不在积压缓冲区里,主节点找不到那个位置的数据了。

3.3 backlog 大小是增量同步的命门

对象积压缓冲区的官方配置名是repl-backlog-size,默认只有 1MB。1MB 听着不小,但放在写流量大的主节点上可能不到一分钟就被环形缓冲区覆盖掉了。

为什么说是环形缓冲区?因为它的存储区域是固定大小循环使用的。新命令不断写入,最老的命令就被顶掉。主节点只会保留最近 N 字节的命令流。举个例子:

  • 复制积压缓冲区当前范围是 offset 10000 到 11024。
  • 从节点重连后上报 offset 10500,落在这个区间内,可以增量同步。
  • 如果从节点上报 offset 9900,说明它需要的命令已经有一部分被顶掉了,只能全量同步。

这个例子能直观看出:backlog 越大,能容忍的断线时间越长,增量同步的成功率越高。

生产环境里怎么定这个大小?我一般会先做个粗估算:

repl-backlog-size = 主节点每秒写入量(bytes) x 预期最长断线时间(秒)

比如主节点平均每秒产生 2MB 写命令,我希望从节点断线 120 秒后重连还能走增量同步,那就得给到 256MB 以上。注意这里算的是写命令的字节量,不是业务写入的数据量。一条更新 100 字节字段的命令,在复制流里可能也要占几十到上百字节,所以用粗略值乘个 1.5 系数更稳妥。

设置方式:

config set repl-backlog-size 268435456

如果实例已经运行一段时间了,Redis 会直接从当前offset开始填充新的环形区间,不需要重启。生效后可以通过INFO replication里的repl_backlog_active和repl_backlog_histlen确认。

我踩过的一个真实教训是:某业务从节点数量有七八个,平时增量同步都正常,但只要某个从节点所在的网络偶尔抖动超过一两分钟,重连后就必然是FULLRESYNC。后来把repl-backlog-size从默认 1MB 调到 128MB,复制风暴立刻消失。查日志的时候看到的关键字一般是:

Trying a partial resynchronization (request ...) Full resync requested

前面的说明在尝试增量,后面说明失败了、退化成全量。如果你在日志里看到这两种状态交替出现,第一反应就应该是去看 backlog 大小,而不是去怀疑网络。

4. PSYNC 协议演进:断点续传能力是怎样一步步变强的

4.1 SYNC 时代的无奈:每次重连都是一次全量

现在大家接触 Redis 基本都是 4.0 以上版本,PSYNC 已经是默认行为。但如果你翻过老代码或者看过更早的运维文档,会发现最初的复制协议只有一个命令:SYNC。

SYNC的逻辑非常简单粗暴:从节点一发SYNC,主节点就BGSAVE生成 RDB 发过去。在这个协议下,从节点只要断开重连,不管断了几秒、缺了多少数据,主节点都得重新生成一份完整快照再全量发一遍。

那个时期的 Redis,从节点数量稍微多一点,只要赶上一次网络抖动,就可能产生连锁反应:每个从节点都触发一次全量同步,每个全量同步都会让主节点 fork 并发送 RDB,主节点 CPU、内存、带宽同时被打满。可以说,SYNC协议是把“简单”写在了脸上,代价全留给了运维。

4.2 PSYNC 与 replid:第一代断点续传是怎么实现的

后来 Redis 引入了PSYNC,核心变化就是给复制流增加了两个“坐标”:replid和offset。从节点可以在断线重连时带上自己上次记住的坐标,主节点只要发现自己手里还留着这段数据,就能从断点继续传,不必每次都全量。

这背后的实现就是我前面讲的复制积压缓冲区。可以说,PSYNC协议的价值和 backlog 是绑定的,没有 backlog 就没有断点续传。

平时用INFO replication能看到主节点上这么一行:

master_replid:8e0d1f3c...

这就是当前复制流的身份 ID。只要主节点维护的这个 ID 不变,从节点带着旧的replid和offset回来,就有机会继续增量同步。

4.3 PSYNC2:主从切换后不再强制全量

但第一代 PSYNC 有一个明显的硬伤:一旦发生主从切换,原本的从节点被提升为新主节点,它自己的replid是全新的,原来那批从节点带着旧主节点的replid再连过来,两边身份对不上,统统退化为全量同步。

设想一个场景:A 是主节点,B 和 C 是 A 的从节点。某天 A 宕机,哨兵把 B 提升为新主节点。C 节点还在正常运行,它手里的数据就差 A 挂掉那一刻没来得及同步的一小段而已。理论上,B 和 C 都从 A 复制了一份几乎相同的数据,C 连到 B 时完全有资格做增量同步。但旧协议不认账,C 会被迫从头开始全量拉一次 B 的数据,浪费大量带宽。

第二代 PSYNC(常被称为 PSYNC2)解决的就是这个问题。它引入了一个replid2的概念:当一个从节点被提升为主节点时,它会保留自己作为从节点时期的主节点replid,把它存到replid2里。C 节点再连过来时,B 会检查:你的replid虽然跟我不一样,但和我保存的replid2匹配,而且你的offset还在我的 backlog 范围内,那就允许增量同步。

这样一来,主从切换后的大部分从节点都能继续走增量数据同步,只有极少情况下才需要全量。这在大型集群里的收益非常明显:少了全量同步,等于少了 N 次 RDB 文件传输,主节点的 fork 压力和网络压力都大大缓解。

可以用一张小表把三代协议的区别说清楚:

能力SYNCPSYNCPSYNC2
初次全量同步支持支持支持
断线重连续传不支持支持支持
主从切换后续传不支持不支持支持
需要 backlog不需要需要需要

5. 主从复制链路中的坑与排查经验

5.1 反反复复的全量同步,先别怪网络

如果你的从节点日志里一直出现“先尝试部分重同步、紧接着又变成全量同步”的状态,第一排查项一定是 backlog 容量。环型缓冲区的特性决定了它只认“最近发过多少字节的命令”,不认“你断线了多长时间”。哪怕只断了几秒,只要这几秒内主节点的写命令字节量超过了 backlog 的上限,重连照样得全量。

排查步骤也不复杂:

# 主节点上查看 backlog 实际使用情况 redis-cli info replication | grep -E "backlog|offset"

重点看几个字段:

  • master_repl_offset:主节点当前写入进度。
  • slave_repl_offset:从节点当前进度。
  • repl_backlog_histlen:backlog 里目前保存的有效命令流长度。

如果你的repl_backlog_histlen已经等于repl-backlog-size,说明环形缓冲已经被写满,里面的内容时刻在滚动淘汰。这种状态下断线重连,增量同步的成功率非常低。解决办法也很直接:把 backlog 调大,同时结合业务写流量估算出合理值。

调整完不是结束,还应该盯两轮:第一轮观察断线重连的日志,确认出现的是+CONTINUE而不是FULLRESYNC;第二轮观察主节点 CPU,确认没有因为频繁全量同步而持续走高。

5.2 过期 Key 不会在从节点上主动删除

这是个非常经典的主从一致性细节。在主从复制中,从节点默认不会主动删除过期的 key。听起来是不是反直觉?如果从节点自己能判断 key 过期,直接删掉不就行了,为什么还要等主节点通知?

原因在于:两个实例的“时间感”不完全一致,如果在同一秒内主节点认为 key 过期、从节点认为还没过期,两边删除时机不一致就会导致数据分叉。所以 Redis 的策略是:过期删除的决策权只在主节点。

主节点的处理方式有两类:

  • 惰性删除:客户端请求某个 key 时,发现已经过期,直接删除并返回空。
  • 定期删除:后台周期扫描一批 key,删除其中过期的。

不管是哪种方式删除,主节点都会把删除行为转成一条DEL命令,通过复制流转发给从节点。从节点收到这条DEL后才会真正把本地这个 key 删掉。

这里有个副作用:从节点在收到主节点的删除命令之前,如果被读到了那个逻辑上已经过期的 key,会发生什么?按我的实测经验,Redis 从节点会直接把这个 key 当不存在处理,返回空结果,但底层数据仍然占着内存。也就是说,从节点会“看不到”这个 key,但内存迟迟不释放。如果你发现从节点的内存占用长时间高于预期,而大 key 扫描也没扫出什么东西,可以怀疑是过期 key 堆积。

解决办法并不复杂:检查主从复制是否正常,确认没有积压很久的延迟,因为从节点必须追平主节点、消化掉积压的 DEL 命令,才会把这些过期 key 清空。另外要注意从节点一定要开启只读模式:

config set replica-read-only yes

如果有人在从节点上手动写入或修改数据,主从数据立刻就会分叉,而且很难追回来。这类问题往往比 backlog 问题更隐蔽,因为业务表现只是一些偶发的数据“对不上”。

5.3 复制风暴与从节点级联控制

所谓复制风暴,是指短时间内大量从节点同时触发全量同步,导致主节点资源被冲垮。典型诱因有两种:一个是主节点重启或切换导致所有从节点同时掉线再重连;另一个是从节点全网段重启,全部在同一时刻重连主节点。

应对思路有几个,我按优先级说:

  1. 调大 backlog,让尽量多的断线重连走增量同步。这是最便宜、最基础的措施。
  2. 错峰重连,靠运维工具控制从节点分批重启,不要一把梭。
  3. 用链式复制,即主-从-从结构。中间的从节点既当从节点又当下一级的主节点,减轻顶层主节点的压力。
  4. 开启无盘复制并设置 delay,多个从节点可以共享一次 RDB 生成结果。

我自己的倾向是:从节点数量超过 5 个的架构,链式复制是值得认真考虑的,尤其是跨机房场景。跨机房带宽本来就紧张,如果每台从节点都直接从主节点拉全量 RDB,那点带宽瞬间就会被吃光。中间建一层“中继从节点”,让它在某个机房内再向下一层复制,能显著降低跨机房的复制流量。

需要注意,链式复制会让最下端从节点的数据滞后时间变大,因为命令要经过两次转发。这属于典型的时间换带宽,是不是划算,得结合业务对读取延迟的要求来权衡。

5.4 平时怎么判断主从状态是健康的

我养成了一个习惯:每次接手一套 Redis 环境,第一件事先把INFO replication的输出完整看一遍。重点关注这么几项:

  • role:是主还是从,别搞反了。
  • master_link_status:up:复制链路是否正常。状态不是 up 就得马上看日志。
  • master_sync_in_progress:0:是否正在全量同步。长时间为 1 说明有大 RDB 在传。
  • slave_repl_offset和主节点的master_repl_offset:两者差值是不是在合理范围内,长期偏大就说明从节点追不上。
  • master_last_io_seconds_ago:最后一次和主节点通信是在多久之前,超过timeout值就要警惕掉线。

这里还有一个经验之谈:从节点追不上主节点,不一定是网络慢,也可能是主节点发给它的命令本身就重。比如一条命令操作大集合的几百个元素,复制流里这条命令的体积很大,从节点执行时又会触发内存分配和哈希表扩容,耗时远超主节点。这种情况下光是调大 backlog 没用,还得优化业务侧的写命令模式,比如把大集合的批量写拆开。

最后分享一条我个人反复验证过的经验:当复制链路出现诡异问题时,不要急着改配置。先把redis-cli info replication的快照保存一份,再把主从两端的日志级别临时调到 debug,复现一次问题,把MASTER <-> REPLICA sync相关日志完整收集起来。八成以上的复制问题,从这几行日志里就能看出真正的走向,剩下的两成,才轮到网络抓包和内核参数排查。这套笨办法帮我在好几次故障里省下了大量半夜时间,也建议你下次遇到类似问题时先试试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询