上周做Redis故障演练,主节点A宕机,从节点B升主。A恢复后,我本以为它会走增量同步------毕竟B的缓冲区足够大,A的偏移量也在范围内。但日志显示:全量同步。这是怎么回事?
如果你也遇到过类似的现象,或者单纯好奇Redis主从复制在故障转移后到底怎么决策的------这篇文章就是为你写的。我们不背八股文,而是从这次排查经历出发,把背后的设计逻辑彻底理清楚。
先理解核心问题:主从复制到底在解决什么?
Redis的主从复制,本质上是在多节点之间保持数据一致。一个主节点负责写,多个从节点复制主节点的数据,提供读服务或作为容灾备份。
但复制不是一次性的,它面临两个核心挑战:
- 网络闪断:从节点和主节点断开几秒又重连,难道要全量同步吗?
- 故障转移:主节点挂了,从节点升为新主,其他从节点怎么同步?旧主恢复后又怎么处理?
这两个问题,引出了Redis复制机制中最核心的两个概念------复制ID(Replication ID) 和复制偏移量(Replication Offset)。
复制ID和偏移量:数据历史的身份证和页码
复制偏移量:精确记录同步进度
偏移量是一个不断递增的字节数,代表复制流中已发送的数据位置。
- 主节点维护一个全局的
master_repl_offset,记录自己总共产生了多少字节的写命令。 - 每个从节点维护自己的
slave_repl_offset,记录自己已经接收并应用了多少数据。
偏移量的核心作用:主节点通过对比自己的master_repl_offset和各个从节点的偏移量,精确判断每个从节点落后了多少数据。
复制ID:数据历史的身份证
复制ID是一个随机字符串,用来标识一份数据集的历史版本。
- 每个Redis实例启动时,都会生成一个唯一的复制ID。
- 从节点完成全量同步后,会继承主节点的复制ID。
- 在同一个复制拓扑中,所有节点最终拥有相同的复制ID。
复制ID的核心作用:当一个从节点重连时,会发送自己记录的复制ID给主节点。主节点通过检查这个ID是否匹配,判断该从节点是否与自己拥有共同的历史数据------这决定了能否进行高效的增量同步。
PSYNC:从全量到增量的进化
在Redis 2.8之前,只有SYNC命令:一旦断线重连,无条件全量同步,效率极低。
Redis 2.8引入了PSYNC命令(Partial Sync,部分同步),核心改进是:从节点重连时,带上自己记录的主节点runid和偏移量。主节点检查:
- runid是否匹配?
- 请求的偏移量是否还在自己的复制积压缓冲区内?
如果两个条件都满足,主节点只发送缺失的增量数据------这就是部分重同步。
但PSYNC仍有局限:
- 从节点重启:重启后内存里的 runid 和偏移量都没了,只能全量同步。
- 故障转移: 主节点A 宕机,从节点 B 被提升为新主。B 生成了一个新的 runid,从节点 C 带着 A 的 runid 来请求同步------B 根本不认识,直接拒绝,C 被迫全量同步。
PSYNC2:复制ID登场
Redis 4.0引入了PSYNC2,用复制ID替代了原来的runid,并且在将从节点切换为主节点的过程中保留原主节点的复制ID。
主节点的复制ID维护方式
在PSYNC2中:
- 主节点拥有一个当前复制ID(
replid)。 - 所有从节点共享这个复制ID。
- 当发生故障转移、从节点升主时,新主节点会生成一个全新的复制ID(
replid),但同时将旧主节点的复制ID保存在replid2中,并记录当时偏移量作为replid2_offset。
这就像是一个人获得了新身份证,但依然保留着旧身份证,以便证明"我还是原来那个我"。
故障转移后的两种命运
再回到文章开始的场景:
- 主节点 A,偏移量 1000
- 从节点 B,偏移量 900(异步复制有 100 条延迟)
- 从节点 C,偏移量 700(异步复制有 300 条延迟)
A 宕机,B 被提升为新主节点。B 生成了新的复制 ID,并保存了 A 的复制 ID ,记录 replid2_offset = 900。B 的缓冲区里还保留着从 A 同步来的、偏移量 0 到 900 的数据。
现在,C 和 A 分别向 B 发起同步请求,命运截然不同。
从节点 C:偏移量 700 → 增量同步
C 发送 PSYNC ID_A 700。
B 的判定:
ID_A是原主节点复制ID ✅- 700 是否 ≤
replid2_offset(900)?✅ - 700 是否还在缓冲区范围内?✅
三个条件全部通过,C 增量同步。
旧主节点 A:偏移量 1000 → 全量同步
A 恢复后发送 PSYNC ID_A 1000。
B 的判定:
ID_A是原主节点复制ID ✅- 1000 是否 ≤
replid2_offset(900)?❌
因为 B 对 ID_A 这个历史数据的认知只到 900。A 的 900 到 1000 这段数据,B 从未见过。如果允许增量同步,A 的内存里会同时保留着旧历史和新数据,在 900~1000 区间产生冲突,集群数据将陷入无法调和的分裂。
宁可全量同步,绝不允许数据分叉。 这是一条不可逾越的底线。
设计哲学:一致性优先于效率
回看整个 PSYNC2 的设计,核心权衡只有一条:为了绝对的数据一致性,可以牺牲旧主节点的恢复效率。
- 对于"落后"的从节点(偏移量 ≤
replid2_offset):伸出援手,让它们增量同步,快速追上。 - 对于"超前"的旧主节点(偏移量 >
replid2_offset):判定数据有罪,强制全量同步,彻底洗白。
这个设计不是"不近人情",而是经过深思熟虑的取舍。在分布式系统中,数据分叉是比性能开销严重得多的问题。一旦出现分叉,没有任何自动机制能安全地将其合并。
写在最后
Redis 主从复制不是一套简单的配置,它是复制 ID、偏移量、复制积压缓冲区、PSYNC2 协议、故障转移决策等多个机制的精密协同。
这次故障演练虽然踩了坑,但也让我真正理解了PSYNC2设计的精妙之处:宁可全量同步,绝不允许数据分叉。下次如果你也遇到旧主恢复触发全量同步,别慌------这不是BUG,是Redis在设计层面为数据一致性划定的红线