Redis 主从同步问题探讨
背景:在不考虑集群状态发生变更的的情况下, Redis 的主从数据同步是怎么做的,以及如何解决 SET TTL 和 SET NX 的主从一致性问题。
主从同步是怎么做的
全量同步
从节点第一次连上主节点,或者断线重连后数据差距太大,会触发全量同步。
主节点 fork 一个子进程在后台生成 RDB 快照,同时把这段时间新来的写命令缓存在 replication buffer 里。RDB 生成完之后通过 socket 发给从节点,从节点加载完 RDB,再把 buffer 里积压的命令回放一遍,追上主节点的状态。
增量同步
全量同步完成后,主节点把后续每一条写命令实时推给从节点,从节点按顺序执行。这个过程是异步的,主节点不等从节点确认就返回客户端成功。
顺序性由什么保证?Redis 主节点是单线程处理命令的,命令执行顺序是全局确定的序列。主节点把这个序列按顺序写入 replication buffer,从节点通过一个持久的 TCP 连接按顺序消费,TCP 本身保证字节流有序不乱序。所以不需要额外的排序机制,顺序天然确定。
断线重连的优化:repl_backlog
如果主从之间网络短暂中断,重连后不一定需要重新全量同步。主节点维护了一个固定大小的环形缓冲区(repl_backlog),记录最近的写命令。重连时从节点带上自己的复制偏移量(replication offset),如果这个偏移量还在 backlog 范围内,主节点只需要补发缺失的那段命令,不用再跑全量同步。如果断线太久、偏移量已经被覆盖了,才会触发全量同步。
主节点还有一个 runid,重启后会变。从节点记录自己同步的是哪个 runid 的哪个 offset,重连时拿这两个值去对比,主节点据此判断能不能走增量补偿。
SET TTL 和 SET NX 的一致性问题
SET TTL:时间漂移怎么解决
SET k1 v1 EX 10 这条命令如果原样发给从节点,从节点执行时已经晚了几毫秒,k1 在从节点上的实际过期时间会比主节点稍晚。
Redis 的解法是传绝对时间戳,在源码 expireGenericCommand 函数里:相对时间命令进来时,Redis 内部立刻把它换算成绝对毫秒时间戳存进 db->expires 字典,复制给从节点时传的是已经算好的绝对时间戳,而不是原始的"再过 10 秒"。这样从节点执行的是同一个绝对时间点,时间漂移问题基本消除。
// expireGenericCommand 里的换算逻辑
if (unit == UNIT_SECONDS) when *= 1000;
when += basetime; // basetime 是当前时间,when 变成绝对毫秒时间戳
SET NX:条件命令的一致性
SET k1 v2 NX 是"key 不存在才写入",这里有两个分支需要分别分析。
分支一:主节点 key 已过期,NX 成功
主节点先通过 propagateExpire 发一条 DEL 给从节点,然后 NX 成功,再发一条 SET 给从节点。从节点按顺序执行 DEL + SET,结果和主节点一致。
分支二:主节点 key 未过期,NX 失败
主节点直接 return,不发任何命令。从节点上这个 key 也还没过期(从节点不主动删过期 key,只等主节点的 DEL),所以 key 依然存在,状态和主节点一致。
NX 失败不发命令,源码 setGenericCommand 里:
found = (lookupKeyWrite(c->db, key) != NULL);
if ((flags & OBJ_SET_NX && found) || (flags & OBJ_SET_XX && !found)) {
if (!(flags & OBJ_SET_GET)) {
addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
}
return; // 直接 return,后面的 setKey、server.dirty++ 都不会执行
}
NX 失败时直接 return,server.dirty 没有增加,命令传播不会触发。
出处:Redis 的 set nx 底层怎么实现的 - SegmentFault,源码来自 Redis 官方仓库
src/t_string.c的setGenericCommand函数。
从节点为什么不会主动删过期 key
上面分支二的结论依赖一个前提:从节点不会主动删过期 key。这一点同样有源码支撑。
expireIfNeeded 函数里:
// 如果本节点是 slave,等着 master 同步 DEL 命令
if (server.masterhost != NULL) return now > when;
从节点走到这里直接返回,不执行后面的删除和 propagateExpire。物理删除只在主节点上发生,主节点通过 propagateExpire 把 DEL 同步过来,从节点才真正删。
Redis 官方文档也有明确表述:
Slaves don't expire keys, instead they wait for masters to expire the keys. When a master expires a key (or evict it because of LRU), it synthesizes a DEL command which is transmitted to all the slaves.
出处:Redis 官方文档 Replication - redis.io,搜索结果引用自 Redis SLAVE过期键策略 - 掘金
结论
不考虑集群状态变更的情况下,Redis 主从数据同步依靠单线程执行顺序 + TCP 有序传输天然保证了命令顺序,不需要额外的日志对齐机制。
SET TTL 问题通过在主节点侧将相对时间换算成绝对时间戳再传播来解决。SET NX 问题通过两个机制共同保证:NX 失败时不传播任何命令;从节点不主动删过期 key,只等主节点的 DEL。这两个机制配合,保证主从在 SET NX 场景下的结果是一致的。