主从复制要解决的问题很直接:从库怎么拿到和主库一样的数据,并且在网络抖动、从库重启之后还能追上。Redis 给了两种手段——全量同步(把整个数据集重新传一遍)和部分重同步(只补发断线期间漏掉的那一小段命令)。这一篇在本机真实搭了一主一从,完整实测了三种真实发生的场景:第一次连接的全量同步、短暂断线后的部分重同步,以及断线太久之后被迫退回全量同步。
过程中还挖到一个大多数教程没提过的新机制:8.x 版本的全量同步默认走的是"双通道"——RDB 数据和实时命令流走两条不同的 TCP 连接。
两种同步方式,权衡的是"传多少数据"和"主库要记多久的历史"。
本质是上一篇讲过的机制:主库生成一份 RDB 快照发给从库。数据集越大,传输和加载越慢,但不需要主库记住任何历史——总能成功,是兜底方案。
主库维护一个"复制积压缓冲区"(replication backlog),记录最近一段时间的写命令。从库断线重连时,只要请求的位置还在这个缓冲区里,直接把缺的那一小段发过去就行。
backlog 默认只有 1MB(真实配置 repl-backlog-size),断线时间太长、或者断线期间写入量太大,旧数据会被新数据挤出缓冲区——这时候部分重同步就不可能了,只能退回全量同步。
本机起了两个真实 Redis 8.10.1 实例(端口 6390 主库、6391 从库),主库先写入 3 个 key,再让从库执行 REPLICAOF。
第一次连接必然是全量同步——从库自己生成的初始复制 ID 不可能和主库的对得上,日志里的 Replication ID mismatch 直接印证了这一点。真正有意思的是接下来那句:is capable of rdb channel synchronization。
经典的全量同步描述通常是"一条连接,先传 RDB,再续上命令流"。8.x 版本默认已经不是这样了。
src/replication.c · redis @ 8.10.1, L3777 * Rdb channel for full sync * * - During a full sync, when master is delivering RDB to the replica, incoming * write commands are kept in a replication buffer in order to be sent to the * replica once RDB delivery is completed. If RDB delivery takes a long time, * it might create memory pressure on master. Also, once a replica connection * accumulates replication data which is larger than output buffer limits, * master will kill replica connection. This may cause a replication failure. * * The main benefit of the rdb channel replication is streaming incoming * commands in parallel to the RDB delivery. This approach shifts replication * stream buffering to the replica and reduces load on master. We do this by * opening another connection for RDB delivery.
翻译一下这段注释在解决什么问题:传统单连接方案里,RDB 传输期间产生的新写命令得先攒在主库的内存缓冲区里,等 RDB 传完才能补发——如果 RDB 传得慢(数据集大、从库慢、网络差),这个缓冲区可能越攒越大,严重时主库会因为这个从库的输出缓冲超限直接把它踢掉,同步反而失败了。8.x 的解法是开两条连接:一条(main-channel)专门实时流式发送命令,另一条(rdb-channel)专门传 RDB 字节——缓冲的责任转移到从库自己身上,主库不用再攒。
用 CLIENT KILL 从主库这边强行断开和从库的连接(模拟网络抖动),从库进程本身没死,断线期间主库继续写入两个 key。
从库自己记住了断线前的复制 ID 和偏移量(239),重连时带着这两个信息去问主库"接着这里发行不行"——因为断线期间只写了两条小命令,远没超过 backlog 的默认 1MB 容量,主库直接说行,补发缺的那一段。断线期间写的两个 key,重连后立刻就在从库上了,不需要重新传一次全量数据。
这次用 SIGSTOP 真的把从库进程冻住(不只是断连接,是整个进程暂停),断线期间主库写入超过 1MB 的数据,让 backlog 把从库还没读过的部分挤掉。
从库想接着偏移量 438 继续,但主库这时候已经写到了 160 万字节开外——1MB 的 backlog 窗口早就把 438 这个位置挤没了,主库直接拒绝部分重同步,退回全量。等这次全量同步完成,两边 dbsize 都是 3005,数据重新对齐。
这也是这段真实源码判断逻辑的字面翻译:
src/replication.c · redis @ 8.10.1, L1044 /* We still have the data our slave is asking for? */ if (!server.repl_backlog || psync_offset < server.repl_backlog->offset || psync_offset > (server.repl_backlog->offset + server.repl_backlog->histlen)) { serverLog(LL_NOTICE, "Unable to partial resync with replica %s for lack of backlog " "(Replica request was: %lld).", replicationGetSlaveName(c), psync_offset); goto need_full_resync; }
用一个简化的字节偏移量模型复现上面三次真实场景的判断逻辑——窗口容量设成 100 万字节,和真实默认值 repl-backlog-size(1,048,576)对齐。
绿色窗口是 backlog 当前还留着的字节范围;从库重连时带来的"想接着哪个位置继续"用一根竖线标出——落在窗口里就是部分重同步(绿色旗标),被挤出窗口就只能全量(红色旗标)。判断规则和上面真实源码里的窗口判断完全一致,过程经过独立回放校验。