☰
Redis主从复制与数据同步机制全解析:从全量同步到增量续传
2026/10/14 1:34:32 网站建设 项目流程

做Java后端这几年,Redis的主从复制始终是一个绕不开的话题。无论是准备架构师岗位的面试,还是排查线上数据不一致的问题,你迟早会被问到一个问题:主从之间到底是怎么同步数据的?网上讲这个机制的文章很多,但大多停留在“主节点生成RDB快照发给从节点,之后增量同步”这种层面,真到出问题时,比如从节点反复全量同步、主从切换后数据错乱,很多人就卡住了。这篇文章是Java架构直通车系列里关于Redis主从数据同步机制的完整拆解,我会从一次全量同步的建立链路讲起,再到断线续传的核心原理、日常运维排障,最后落到Java架构里读写分离和高可用的落地场景。内容偏实战,包含了我在实际项目中踩过的坑和沉淀下来的检查清单,适合正在学习分布式缓存原理、或者已经在维护Redis集群的后端同学。

1. 主从同步解决的本质问题:读扩展、容灾,以及一个常被忽略的前提

1.1 单点Redis的三个短板

很多人最开始接触Redis就是单机版:一个实例、一把梭。单节点在业务规模小的时候确实够用,可一旦流量上来,问题就藏不住了。

首先是可用性。Redis进程挂了,内存里的数据还在,但磁盘上的RDB/AOF是上一次持久化周期的状态,业务却已经彻底不可用了。重启时间从秒级到分钟级,对线上来说就是事故。更麻烦的是,如果服务器的硬件故障不是进程崩溃而是整机宕机,数据恢复可能压根就不存在了。

其次是读性能。Redis本身是单线程模型(指命令执行),单节点能扛的QPS通常到十万级,但很多读多写少的业务,比如商品详情、用户会话、配置中心,读请求可能是写请求的几十倍。一台机器扛不住这个读压力,你能想到的扩容手段就是再加一台机器。

第三是运维操作。主节点要做RDB快照,fork子进程写磁盘,如果内存很大,fork会有短暂的阻塞,高并发时段容易造成请求抖动。这些问题靠单机调优解决不彻底,架构上就要引入多节点。

主从复制解决的核心问题就是这三件套:把读流量分散到从节点、在硬件故障时能快速切换、把快照重活放到从节点上分摊。它是Redis高可用架构的第一步,也是之后哨兵和集群的地基。

1.2 主从复制和持久化是两件事

这是我最常遇到的一个认知误区。有人问:我已经开了AOF和RDB,为什么还要主从复制?持久化和复制解决的问题完全不同。

持久化解决的是“进程挂了,怎么把数据从磁盘恢复到内存”,它还在同一台机器上。主从复制解决的是“这台机器彻底没了,怎么让另一台机器顶上”。单靠持久化,换成另一台机器的时候,你需要手动把磁盘文件拷贝过去,这个拷贝过程里新的写请求怎么办?挂了两个小时之后拷贝过去,等于丢了两个小时数据,业务上不可接受。

主从复制和持久化更多是配合关系。从节点接收完RDB之后,仍然会开启自己的持久化配置。这样即便主节点和所有从节点在同一个机房出现了物理故障,至少每个节点上还有一份落盘的RDB/AOF可以恢复。所以正确的架构姿势是:持久化保证单点恢复,复制保证数据冗余和故障转移,两者缺一不可。

1.3 读多写少场景下的吞吐扩展

单主多从的架构能支撑的读能力提升是肉眼可见的。假设一个主节点能支撑8万读QPS,挂两个从节点,读能力理论上能到24万QPS,写仍然走主节点。业务请求从应用层分流,读请求打到从节点,写请求打到主节点。

这里要提前打个预防针:主从复制是异步的,从节点上的数据永远可能比主节点旧一点点。读从节点得到的不是严格实时的数据,这决定了你在Java应用里设计读写分离时,必须想清楚哪些请求能容忍延迟,哪些请求必须穿透到主节点。后面第6章我会专门讲这个问题。

2. 从第一次握手到全量装载:主从复制的完整建立链路

2.1 REPLICAOF之后,从节点心里装了一张流程清单

Redis 5.0之后,把slaveof改成了replicaof,语义上更准确:节点建立主从关系。在从节点执行:

replicaof 192.168.1.10 6379

或者写进配置文件:

replicaof 192.168.1.10 6379 masterauth yourpassword

这一步做了之后,后台会发生一串事情,远不是一个RDB文件拷贝那么简单。完整的连接建立过程是这样的:

  1. 从节点把主节点地址保存下来,并记录自己处于“待连接”状态。
  2. 定时任务尝试与主节点建立socket连接。
  3. 连接建立后,从节点发送PING,确认主节点能正常响应命令。
  4. 如果主节点设置了requirepass,从节点需要用masterauth做身份认证,认证失败连接直接断开。
  5. 认证通过后,从节点把自己的监听端口告诉主节点。
  6. 从节点发送PSYNC命令,带着自己当前的复制ID(replid)和复制偏移量(offset)。
  7. 主节点根据PSYNC参数,决定返回FULLRESYNC(全量同步)还是CONTINUE(部分同步)。

这一套握手流程,任何一个环节出问题,表现都是master_link_status从up变成down。比如最常见的密码不匹配,主从连接建立了,PING也通了,卡在认证这一步,日志里会出现“MASTER <-> REPLICA sync started: Non AES”之类的提示,实际上是在反复握手失败。

2.2 全量同步阶段三件事:BGSAVE、传RDB、补增量

当从节点是第一次接入主节点,或者主节点判断无法做部分同步时,就会走全量同步。主节点内部大概经历这样三个阶段:

第一阶段:主节点触发BGSAVE,fork出一个子进程,把当前内存里的完整数据快照写入RDB文件。这个阶段耗时取决于内存大小。子进程写RDB期间,主进程继续正常服务所有写命令。

第二阶段:RDB文件生成完毕,主节点把文件通过网络发送给从节点。从节点收到后,先要清空自己当前的所有数据,再把RDB文件载入内存。

第三阶段:关键点来了。从BGSAVE那一刻开始,主节点每收到一条新的写命令,都不会丢掉,而是把这些命令追加到复制缓冲队列里。等RDB传输并装载完成后,主节点把这个缓冲队列里的所有命令逐条发送给从节点,从节点执行一遍,最终状态和主节点保持一致。

第三阶段是主从复制设计里最容易被忽略、也最体现功力的地方。如果只传RDB不补增量,那么从BGSAVE开始到从节点装载完成之间的所有写操作就会丢失,主从数据会出现很长一段窗口期的不一致。必须通过复制缓冲区把这些增量命令缓存起来,在RDB装完之后回放。

2.3 为什么主节点要把命令先缓存在复制缓冲区而不是直接踢给从节点

从概念上,主节点维护着两种跟复制相关的缓冲结构,一个是repl_backlog,一个是per-replica output buffer。很多文章把这两个混在一起讲,这会导致排查问题时误判。

per-replica output buffer是每个从节点独享的命令发送缓冲区。主节点在每次事件循环里,会把要传播的命令写入这个缓冲区,然后尽量把缓冲区里的数据发送给对应的从节点。如果某一瞬间某个从节点消费速度跟不上,命令就会积压在这个缓冲区里。缓冲区有大小上限,可以从配置里查到相关参数,如果积压超过限制,主节点会认为从这个从节点同步已经不可靠,直接断开连接,接下来往往就是一次新的全量同步。

repl_backlog是后面为了断线续传设计的公共环形缓冲区,它不完全是为了正在连接的从节点服务,而是为了“断线之后还能续上”服务的。我把它放在下一章展开,因为它涉及的是主从复制从2.8版本以来最重要的机制演进。

3. 断线续传的底层秘密:replid、offset与repl_backlog

3.1 SYNC时代为什么让人崩溃

在Redis 2.8之前,主从同步只有一条路:全量SYNC。从节点第一次连接是全量,中途网络抖了一下,断了,重连之后还是一整轮全量同步。全量同步的成本大家知道:主节点fork子进程、写RDB、传网络、从节点清空再装载。

如果主节点下面挂了四五个从节点,网络一抖动,所有从节点几乎同时重新做全量同步,主节点要在短时间内fork多次、发送多份RDB,内存、CPU、带宽瞬间被打满,这就是所谓的复制风暴。早期维护Redis的人,对“每次断线都是灾难”这句话应该有很深的体会。

所以2.8引入PSYNC,核心思路是:断线后,如果我还记得我从哪里断开,能不能只补上我缺的那部分命令?

3.2 PSYNC:用复制ID和偏移量做增量续传

PSYNC的全称是Partial ReSynchronization,它引入了两个关键概念:复制ID(replid)和复制偏移量(offset)。

复制ID是主节点生成的一个40位十六进制字符串,标识当前这段复制历史的唯一身份。主节点每次启动时会生成一个新的replid,所以它天然带有“这轮服务的起始状态”的语义。

复制偏移量则是一个不断累加的数字。主节点每向从节点发送一个字节的数据,自己的master_repl_offset就加多少;从节点每接收并应用一段数据,自己的slave_repl_offset也会相应增加。两边只要偏移量一致,就代表数据是一致的。

从节点重连后,带着自己记录的replid和offset去找主节点说:我之前是从你这里同步的,一直同步到偏移量X,现在我想从X继续往下补。主节点一看,replid对得上,且X落在自己的repl_backlog缓冲范围内,好了,只把从X开始的后续命令发给从节点就行。这一套流程就叫部分重同步。

3.3 repl_backlog:环形缓冲区如何决定能否部分重同步

repl_backlog可以理解为主节点内存里的一块环形队列,容量由repl-backlog-size配置,默认是1MB。主节点每次往从节点传播命令时,也会把同样的命令写入这个环形队列。当队列写满,最老的命令会被新命令覆盖。

从节点断开后,主节点不会停止向repl_backlog里写入新命令。如果从节点很快回来,它请求的offset还在队列范围内,那就能部分同步;如果断开太久,offset已经被环形队列覆盖冲掉了,主节点只能望洋兴叹,老老实实重新全量同步。

从这里可以得出一个运维上的重要判断:repl_backlog_size决定了从节点允许多久断线而不触发全量同步。1MB听起来不小,但写放大场景下,一秒就能被刷满。线上业务如果是高写入,这个配置需要跟着权衡。比如预计网络抖动恢复最多30秒,期间主节点每秒产生2MB写命令,那backlog至少得设64MB以上,留出余量。

# redis.conf 示例 repl-backlog-size 64mb

注意,环形缓冲区是主节点上的公共区域,不是单独为某个从节点准备的。多个从节点断线重连,只要它们的offset都落在范围内,都能做部分同步。这才真正缓解了复制风暴。

3.4 PSYNC2演进:主从切换后不再推倒重来

2.8版本的PSYNC解决了断线续传,但有一个场景仍然要全量:主从切换。比如哨兵检测到主节点宕机,把一个从节点提升为新主。此时旧主节点恢复网络回来,它已经变成了整个复制历史里的“前任”,带着的还是换上前的主节点replid。新主节点一看这个replid不是自己的,直接拒绝部分同步,于是旧主只能清空数据,从新主重新全量拉取。

Redis 4.0引入了PSYNC2,核心设计是主节点不仅要保存当前的replid,还要记住自己曾经用过、但现在不再使用的历史replid,也就是second_replid。主从切换后,如果旧主节点带着之前的replid回来,新主节点的second_replid里恰好还留着这个ID,且偏移量对得上,就可以做部分同步,把整个集群从“切换后全员重建”的灾难中救回来。

PSYNC2还处理了从节点升级为主节点的场景。从节点在晋升时会保留自己作为从节点时期的复制历史,并把它作为second_replid。某种意义上,复制ID维护的是一整条血缘链:你只要曾经在我这条复制历史上同步过,哪怕角色变了,我都能认出你。这是理解现代Redis主从复制机制最关键的一个心智模型。

4. 全量同步之后每一天:命令传播、心跳与一致性观察

4.1 命令传播的本质:在主节点上重放一样的写操作

全量同步完成只是开始,此后主从之间进入日常的命令传播阶段。主节点每执行一条写命令,除了更新自身内存,还会把这条命令写进每个从节点对应的输出缓冲区,最终以Redis文本协议格式发到从节点,从节点按顺序执行一遍。

这套机制跟AOF的记录方式很像,本质都是在跑一串命令流。好处是语义简单、逻辑天然有序;代价是从节点需要完整执行命令,存在CPU开销。Redis命令执行本身是单线程的,所以从节点处理复制命令的速度,直接决定了它能不能跟上主节点。

有一个值得注意的细节:命令传播不是主节点执行完立即逐条发出,而是批量攒着,在事件循环里统一发。这是为了减少网络包数量。如果从节点消费慢,积压就会体现在前文说的replication buffer里,极端情况主节点会断开这个慢从节点。

4.2 心跳机制:谁在确认谁还活着

主从连接建立且数据同步稳定后,双方不是相安无事地干等,而是保持着固定频率的心跳。

从节点默认每10秒向主节点发送一次REPLCONF ACK,汇报自己当前的slave_repl_offset。这个汇报有两个作用:一方面让主节点知道从节点还活着、应用到了哪个位置;另一方面帮助主节点清理已经不再需要的输出缓冲区。

主节点侧同样有心跳检测。如果超过repl-timeout指定的时间(默认60秒)没有收到从节点的任何响应,主节点会认定这个从节点已经失联,主动断开连接。从节点也会在同一机制下判断主节点是否失联。

还有一个日常监控会用的指标是lag,它表示主节点最后一次收到从节点ACK距离当前的时间,单位秒。lag为0说明刚刚汇报过,网络连通良好;lag持续上涨,就该开始查网络和从节点负载了。

4.3 从节点只读、主从延迟与WAIT命令

默认配置replica-read-only是yes,从节点只接受读请求,任何写命令都会被拒绝。这个设计不是Redis矫情,而是为了保证复制方向单一,防止从节点数据被外部写脏后导致主从状态无法对齐。如果你确实要在从节点临时开写,必须对一致性风险有充分认知。

主从延迟是绕不开的现实问题。命令从主节点发出,经过缓冲、网络传输、从节点排队执行,这段延迟正常情况下毫秒级,但网络抖动、从节点执行慢命令、大key复制都会让延迟放大。业务上如需判断主从是否追上,可以在从节点上用info replication查看master_repl_offset和slave_repl_offset的差值,差额越大延迟越大。

Redis也提供了WAIT命令,语法是WAIT numreplicas timeout,阻塞当前客户端,直到指定数量的从节点确认收到本次写入,或者等到超时。它可以在一定程度上提高写后读的一致性,但它不是严格意义上的强同步方案:从节点A收到命令和A执行命令不是一回事,而且主节点故障时已确认的写入仍有丢失可能。生产环境不要因为用了WAIT就放松对业务一致性的设计。

5. 主从复制实战排查:一条线上问题背后的复盘路径

5.1 用info replication快速切片检查

不管出什么问题,我第一件事都是连上节点执行info replication。这个命令返回的信息非常集中,主节点关注connected_slaves、master_repl_offset、repl_backlog_active;从节点关注master_link_status、slave_repl_offset、master_last_io_seconds_ago。

举个例子,主节点输出里看到某个从节点的state显示online,但lag一直上涨,说明这个从节点虽然TCP连接没断,但数据处理已经跟不上。再配合主节点上redis-cli --latency去测网络往返,基本能把问题定位在“网络丢包”还是“从节点慢”。

另外有一个排查习惯:不要在高峰期直接在主节点执行bgsave或debug reload来验证RDB生成,尤其内存几十GB时,fork的阻塞风险极高。可以在从节点上测试,或者用bgsave的异步特性观察fork耗时。

5.2 反复全量同步排查:缓冲区越小,越容易触发

线上最经典的问题就是从节点反复全量同步。表现为从节点刚同步完,过一段时间又进入全量,循环往复,主节点被拖得QPS下降。

排这个问题的路径要清晰。先看主节点日志里每次全量同步的原因。如果断线时间很短却仍然全量,基本可以怀疑repl_backlog_size设置过小,offset被环形队列覆盖。我在一个模拟项目X里就遇到过:应用突然开始批量写大集合,单条命令产生几百KB的复制流量,默认1MB的backlog一秒就被冲掉,从节点稍微断个几秒回来,offset就找不到了。

当时把repl-backlog-size从1mb调成128mb,全量同步次数立竿见影地降了下去。调整这个参数不需要重启,但要注意它是主节点配置,多个从节点共享。计算方式可以粗算:业务峰值每秒产生的写命令字节数,乘以你希望容忍的断线秒数,再乘2到3的余量系数。

另一类反复全量是因为从节点消费太慢,导致主节点的输出缓冲区爆掉,主节点主动断开连接。这种情况光调大backlog没用,要看从节点是不是在执行慢命令、是否在做大key迁移、是否磁盘有问题导致无法及时写AOF。

5.3 主从切换后的数据一致性风险

上面提到的PSYNC2让切换后恢复变得平滑,但切换本身仍然有数据丢失窗口。主节点异步复制,意味着从节点大概率没有拿到最后几毫秒的写命令。哨兵选出新主节点后,这些未被复制的写请求就永久丢失了。

这个风险有两个层面。架构层面,Redis官方提供了min-replicas-to-write和min-replicas-max-lag参数:当主节点连接的从节点数量少于指定值,或从节点滞后超过指定秒数时,主节点直接拒绝写请求。这等于在一致性和可用性之间做了取舍,换取的是“丢数据的窗口可控”。

业务层面,Java应用对写结果要做补偿设计。比如下单成功后,如果主从切换丢了这条写,业务侧需要有重试或对账机制捞回来。读写分离场景下尤其要在切换窗口容忍短暂的旧数据,比如商品库存扣减成功但查询返回旧值,这类问题要提前跟产品对齐。

5.4 网络抖动、masterauth、大key……那些年踩过的坑

还有一些看似不起眼、却频繁翻车的坑,我列几个典型的:

网络抖动是最常见触发点。主从节点在同一个机房,交换机端口闪断几秒钟,从节点判断主节点超时,连接断开。如果backlog设置合理,回到部分同步没问题;但如果是大促销期间写流量暴涨,backlog很快被冲掉,就变成了全量同步。所以大促前的Redis巡检,我建议重点检查repl-backlog-size和输出缓冲区相关指标。

masterauth配置错误也能折腾半天。主节点设了requirepass,从节点配置masterauth时多了个空格,或者密码含有特殊字符没有正确转义,日志里只会显示认证失败,不细看很难发现。

大key是另一个隐形杀手。某个Hash里有几百万字段,即使一个写命令也可能产生巨大复制负载,命令传播耗时增长,从节点越拖越慢。这个问题没有复制层能解的招,只能在数据模型设计上拆key、分片。

最后是磁盘瓶颈。从节点默认开启RDB保存,如果磁盘IO本身就高,载入RDB或写AOF会很慢,复制滞后就容易被误判为主从断线。看到lag上涨先查磁盘,别急着骂网络。

6. 从主从复制到高可用架构:在Java项目里怎么用、怎么看

6.1 主从复制是哨兵和集群的地基

很多同学学Redis高可用是从Sentinel和Cluster开始的,容易忽略一个事实:不管上层架构多复杂,数据在节点间的流动方式仍然是主从复制。哨兵负责监控和故障转移,但它不复制数据,复制数据靠的是哨兵控制下的主从关系。Redis Cluster里每个分片也是一主多从,主节点故障时从节点晋升,底层用的同样是全量同步、增量传播那套机制。

所以在排障Redis Cluster问题时,经常发现问题根源还是主从复制:一个分片的主从复制积压了,另一个分片的从节点掉线了。理解主从复制,等于理解了整个Redis高可用体系的受力点。

从架构演进角度看,顺序是:单机、主从、哨兵、集群。每一步都是在上一层的稳定性前提下做增量。主从复制如果配置脆弱、backlog过小、主从延迟失控,那么哨兵和集群再怎么聪明也白搭,因为复制本身就在持续制造新的不一致。

6.2 Java应用侧读写分离的落地与取舍

主从复制在Java应用里最直接的落地就是读写分离。以Spring Data Redis为例,常规做法是配置两个连接工厂,主库连接写操作,从库连接读操作,然后通过路由在运行时选择数据源。

但我要特别强调两个取舍。

第一个是延迟容忍度。订单创建后立刻查订单状态,走从库可能查到“不存在”,这是复制延迟造成的。我的习惯是在事务写后紧跟着的读请求强制走主库,其余普通查询才允许走从库。用ThreadLocal或者请求上下文标记“是否刚刚发生过写”,是一个不算复杂但很实用的方案。

第二个是从库故障时的降级。如果所有读请求都依赖从库,从库一挂,读能力直接崩溃。线上至少保留一个从库兼作冗余,并且在应用层配置从库连接池的健康检查,失败时自动摘除,读流量临时切回主库。这个降级开关必须提前演练,否则真出事时改配置本身就够你忙活十几分钟。

6.3 最后一份配置与监控清单

这篇文章快收尾了,我分享一份自己在实际项目里沉淀下来的清单,可以直接拿去对照检查。

配置层面:masterauth必须与主节点requirepass一致且无多余字符;repl-backlog-size按峰值写流量估算并留足余量;repl-timeout不要改得过小,否则网络瞬断会导致误判;从节点保持replica-read-only yes;有条件时开启repl-diskless-sync,减少RDB落盘对主节点的IO冲击。

监控层面:每小时记录一次主节点的master_repl_offset与各从节点的slave_repl_offset,差值为0视为正常,有持续差值就要定位;观察主节点connected_slaves数量是否稳定;定期检查repl_backlog_histlen是否经常逼近repl_backlog_size,如果经常逼近,就该调大backlog了。

日常操作层面:不要在主节点高峰期执行bgsave;不要用kill -9重启Redis节点;做主从切换演练时,先观察从节点复制偏移量是否追平,再决定是否让它上位;每次调整主从配置都用info replication验证生效。

主从数据同步机制看起来只是几个配置项和一条PSYNC命令,但它背后串联着RDB快照、环形缓冲区、网络传输、心跳检测、故障恢复这一整条链路。把这条链路想透彻了,再回头去看Sentinel、Cluster和Java应用侧的读写分离,都会轻松不少。希望这篇拆解对你有用。

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

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

立即咨询