Redis主从复制的原理是什么?
作者:程序员马丁
Ragent AI —— 从 0 到 1 纯手工打造企业级 Agentic RAG,拒绝 Demo 玩具!AI 时代,助你拿个offer。
回答话术
当从节点初次连接到主节点,或者掉线重连后进度落后较多时会进行一次全量数据同步。此时,主节点会生成 RDB 快照并传输给从节点,在此期 间,主节点接受到的增量命令将会先写入 replication_buffer 缓冲区,等到从节点加载完 RDB 快照的数据后,再将缓冲区的命令传输给从节点,以次完成初次同步。
当从节点掉线重连后,如果进度落后的不多,将会进行增量同步。主节点内部维护了一个环形的固定大小的 repl_backlog_buffer 缓冲区,它用于记录最近传播的命令。其中,主节点和从节点会分别在该缓冲区维护一个 offset ,用于表示自己的写进度和读进度。当从节点掉线重连后,将会检查主节点和从节点 offset 之差是否小于缓冲区大小,如果确实小于,说明从节点同步进度落后不多,则主节点将该缓冲区中的两 offset 之间的增量命令发送给从节点,完成增量同步。
当主从节点完成初次同步后,将会建立长连接进行命令传播。简单的来说,就是每当主节点执行一条命令,它就会写入 replication_buffer 缓冲区,随后再将缓冲区的命令通过节点间的长连接发送给对应的从节点。
问题详解
1. 全量同步
一般来说,当两个节点第一次建立主从关系的时候,一定会触发一次全量同步。整个过程大致分为四步:
- 从节点向主节点发送 psync 请求获取 runID 和 offset;
- 主节点返回自己的 runID 与该从节点的 offset;
- 主节点生成 RDB 文件,并传输给从节点,从节点加载文件后获取全量数据。
- 主节点确认从 节点加载完 RDB 文件后,将这期间缓存的增量命令发送给从节点,从节点加载完毕后结束第一次同步。
1.1. 获取 runID 与 offset
当我们对一个 Redis 实例使用 replicaof 让指定从节点与主节点构成主从关系时,从节点将会根据你指定的 host 和端口号请求对应的服务实例,并向主节点发送 psync {runID} {offset} 请求。
其中,runID 是主节点启动时生成的标识 ID,而 offset 则表示当前从节点从主节点复制数据的偏移量,也就是复制进度。最开始的时候,由于是第一次同步,因此从节点并不知道主节点的 runID,因此 runID 为 ?,由于也还没有复制过数据,因此 offset 为 -1。
当主节点响应请求时,将会返回主节点自己的 runID 和当前从节点的复制进度 offset。并且因为是首次同步,因此主节点会返回 FULLRESYNC 响应,表示从节点需要进行全量同步。
1.2. 加载 RDB 数据
在前文 Redis 的持久化部分,我们提到过,Redis 恢复数据或者进行主从同步的时候是通过 RDB 文件完成的,实际上指的就是全量同步。
在从节点接受了响应以后,主节点将会执行 bgsave 去生成一个 RDB 文件——这个流程与正常生成 RDB 文 件一致——并发送给从节点,从节点接受后,由于是全量同步,因此会先清空自己的已有数据,然后再加载 RDB 文件中的数据。
此时,从节点已经同步过来了主节点的大部分数据,不过由于 RDB 实际上只是一个快照,因此在生成 RDB 文件期间主节点的增量数据实际上还没有被从节点获取。
关于 Redis 是如何生成 RDB 文件的,请参见:✅ Redis 的 RDB 是怎么实现的?
1.3. 获取增量数据
在主节点生成 RDB 文件期间,由于 RDB 文件是通过子进程异步生成的,因此在这个过程中主节点仍然还在正常的处理请求,这部分的增量命令将会写入 replication buffer 缓冲区。
当从节点加载完 RDB 中的数据后,将会向主节点发送确认消息,此时主节点会再将缓冲区中的命令发送给从节点,从节点执行完这部分增量命令后,数据即与主节点基本一致,则全量同步完成。
不过,由于主从的更新已有延迟,因此无论如何数据是很难保证完全一致的,不过这就是后面命令的正常传播和增量同步的事情了。
2. 命令传播
当主从之间根据 psync 请求完成第一次全量或增量同步后,就会保持长连接,此后,将会通过长连接进行命令传播。
在这个过程中,主节点将会在每次执行完一个命令后,分别写两个 缓冲区:
- replication_buffer:主从复制缓冲区,主节点每拥有一个从节点,就会有一个对应的缓冲区,要传播给从节点的命令会先写入该缓冲区,随后在通过长连接发送给从节点。
- repl_backlog_buffer:最近传播命令缓冲区,一个主节点只有一个,用于记录主节点最近传播出去的命令,主节点和全部从节点都会分别在上面维护一个 offset,用于表示自己已经写入或读取的命令进度。
这里 replication_buffer 实际上就是 IO 操作通常都会有的缓冲区,而 repl_backlog_buffer 则是为了解决从节点掉线重连后的增量同步问题。
3. 增量同步
repl_backlog_buffer 是主节点中一个比较特殊的缓冲区,和每个从节点都有一个 replication_buffer 不同,一个主节点只会有一个 repl_backlog_buffer 。
repl_backlog_buffer 是一个固定大小的“环形”区域,当主节点写入数据时,它会使用 master_offset 记录自己当前已经写到哪个字节,对应的,从节点也会有一个 slave_offset表示从节点已经读到的哪个字节。当主节点写的数据超过缓冲区大小后,它将会覆盖最早写过的内容。
因此,当从节点要与主节点进行同步时,仅需要在 psync 请求时给出自己的 slave_offset 即可,主节点将计算其与 master_offset 的差值:
- 如果差值大于
repl_backlog_buffer的大小,说明两者数据已经差了很多,那么需要重新进行全量同步。 - 如果查找小于
repl_backlog_buffer的大小,说明数据差还在容许范围内,则主节点将返回CONTINUE响应,让从节点准备进行增量同步,并把repl_backlog_buffer中的差值部分写入replication_buffer并发送给从节点,让从节点把同步进度追上来。
简单的来说,如果从节点最后一次读取的命令可以在上面找到,那么说明从节点的数据没有落后太多,因此可以增量同步,否则就需要进行全量同步。
当主从链接断开后,从节点会重新发送 psync 请求向主节点要求同步数据,在 2.8 之前总是会使用全量同步,而在 2.8 及以后的版本,将会使用增量同步。
综上,我们不难意识到,如果你的网络不太稳定,那么最好把 repl_backlog_buffer 调大一些,这样可以尽可能的避免从节点掉线重连后需要频繁的进行全量同步。
4. 主动切换导致数据丢失
当一个 Redis 节点作为从节点使用时,为了保证始终能够正确从主节点同步数据,因此它的 maxmemory 配置将不会生效。
这也就意味,当你的从节点的 maxmemory 小于主节点的 maxmemory 配置时, 从节点可能会从主节点同步到超过其 maxmemory 配置的数据量。此时一旦发生主从切换,从节点就会意识到自己的使用内存已经超过最大内存了,因此会立刻开始进行内存淘汰。在这种情况下,节点可能会导致大量的数据丢失,并且在较长一段时间内处于不可用状态。
总而言之,当你使用主从时,记得要让主节点和从节点的配置始终保持一致,避免因为主从切换导致不可预料的后果。