Redis主从复制核心机制:RunID与Backlog决定全量或增量同步 1. 揭开Redis主从复制的面纱从一条命令说起先抛个问题Redis单机性能再强也有扛不住的时候。缓存雪崩、磁盘损坏、机房断电任何一个故障都可能导致整个业务链路直接瘫痪。这时候你会想到什么主从复制。没错就是那个被无数人挂在嘴边但真正理解其内部机制的人并不多的Redis高可用基石。提到Redis主从复制绝大多数人第一反应是执行一句SLAVEOF命令然后从节点就开始同步数据了。这事儿看起来简单到令人发指——主节点写从节点跟着抄一份。但如果你真的以为主从复制只是“把数据拷贝一份”这么简单那后面遇到同步延迟、数据不一致、增量同步失效、全量同步风暴这些生产事故时你会连排查方向都没有。我之前在维护一套生产环境的Redis集群时就遇到过这么个事儿某天下午某台从节点因为网络抖动断连了大概一分钟重连之后本以为它会自动增量同步补齐数据结果日志里赫然出现了全量同步的字样。当时我就愣住了——斷連才一分钟怎么就触发全量同步了后来深挖才发现是复制积压缓冲区Backlog太小复制流的偏移量已经超出了缓冲区范围主节点只能无奈地放弃增量同步直接走全量同步流程。那一分钟积压缓冲区的数据被新的写入覆盖掉而出于某种原因从节点发来的偏移量在主节点看来已经“过期”了于是触发全量同步。那一次事故让我意识到主从复制背后还有太多值得解剖的细节。这篇文章我想从头到尾把这些细节讲透不只讲SLAVEOF怎么用更要把 Redis 主从复制背后的运行机制挖出来——RunID 到底起什么作用、Backlog 又是如何决定增量同步能否成功、复制偏移量怎么计算、主从握手经历了哪些阶段、断线重连后增量同步的触发条件到底是什么。文章里没有什么高深莫测的理论全都是我从实战和源码阅读中提炼出来的经验。无论你是刚入门的 Redis 使用者还是已经在维护生产集群的工程师这篇文章都能给你一些参考价值。2. 主从复制整体架构到底是“复制”还是“备份”2.1 一主多从的拓扑结构先理清楚一个概念Redis 的主从复制并不是传统意义上的“双机热备”而是一主多从的结构。主节点Master负责处理写请求从节点Slave负责复制主节点的数据同时可以对外提供读服务。这种拓扑结构最直接的收益就是读写分离——把读流量分摊到多个从节点上减轻主节点的压力。生产环境里我见过不少团队把主从复制当成“数据备份”来用其实这是不严谨的。主从复制解决的核心问题是数据冗余和读扩展但它不等于持久化备份。如果主节点出现了物理故障比如磁盘整个坏了你不可能靠从节点找回在复制过程中丢失的数据因为主从复制是异步的这意味着主节点上已写入但还没来得及传播到从节点的数据在故障发生时就是永久丢失了。所以一个成熟的Redis高可用方案里主从复制通常与持久化RDB AOF配套使用。先保住数据不丢再用主从复制实现故障转移和读扩展。两者各司其职缺一不可。2.2 全量同步与增量同步的适用场景Redis 主从复制看起来只有两种方式全量同步和增量同步。但实际面试和文档里这两个术语对应着更细分的三类过程全量同步从节点第一次连接主节点或者复制流严重断档时主节点生成RDB快照发送给从节点从节点加载RDB完成初始数据对齐。增量同步从节点已经有一部分数据、只是落后于主节点少量偏移量时主节点将复制积压缓冲区内积压的写命令流发送给从节点从节点继续追加执行追平偏移量。部分重同步也可以看作增量同步的具体实现断线重连后从节点带上自己的复制偏移量请求主节点继续同步主节点判断偏移量仍然在自己的Backlog有效范围内于是仅发送断连期间积压的写命令。很多人以为全量同步只发生在第一次建立主从关系时其实不是。只要主节点的repl_backlog缓冲区中不包含从节点请求的偏移量之后的数据主节点就会强制退化到全量同步。这也就是我在开头提到的那次事故的根源所在。说个更通俗的类比全量同步就像你要和同事对齐一份文档你们俩手里的版本差得太多索性把最新版整个发过去覆盖一遍增量同步就像你们俩的版本只差了几行修改只需要把这几处变更传过去合并一下就完事。Backlog 就是主节点上那个“最近若干条修改记录”的暂存区它决定了你们的版本差能不能通过“传变更记录”的方式弥补。2.3 为什么说主从复制是异步的Redis 默认的主从复制是异步复制——主节点处理完写命令后直接返回客户端结果不会等待从节点确认。这个设计在性能上极为高效但也引入了一个副作用主从切换时可能会有少量数据差异。实际生产中这个问题表现为“主从切换后的数据回滚”。举个例子客户端写入了一个key主节点已经返回OK但从节点还没来得及复制这条命令这时候主节点宕机哨兵把从节点提升为新主节点那这条命令就在新主上消失了。对某些业务来说这是不可接受的。Redis 也提供了一定程度的缓解手段比如配置min-replicas-to-write和min-replicas-max-lag——只有当从节点数量足够多且延迟足够低时主节点才接受写请求否则直接拒绝写入。这种方式本质上是用牺牲可用性的代价来换取数据一致性需要结合业务场景来权衡。3. RunID主从复制的身份标识缺了它就乱套3.1 RunID 是什么从哪里来RunID 是 Redis 主节点在启动时生成的随机标识符一个 40 位十六进制字符串本质上是用来唯一标识一次“主节点运行周期”的。每次主节点重启这个ID都会改变。从节点的视角来看RunID 决定了它当前跟随的是不是“同一个主节点”。这个机制非常重要如果主节点重启RunID 变化从节点就会识别出主节点已经不是之前那个实例进而重新发起全量同步。为什么 RunID 变化了就一定要全量同步原因在于主节点重启后它不知道自己重启期间从节点接收了哪些数据也无法确认从节点的复制偏移量是否还与自己一致尤其主节点重启前可能还有未持久化的写命令。为了保证数据一致性的底线Redis 选择了最保险的方式——全量重来。3.2 RunID 在主从握手过程中的角色先来看主从复制的握手流程一次完整的初始同步大致经历以下阶段从节点保存主节点的host和port建立 socket 连接。从节点发送PING命令主节点响应PONG确认网络通畅且主节点服务正常。从节点携带自己的配置信息执行REPLCONF listening-port port告诉主节点自己的监听端口。从节点发送REPLCONF capa eof capa psync2声明自己支持的复制能力比如支持PSYNC协议。从节点执行PSYNC runid offset其中runid是从节点记忆中的主节点IDoffset是它当前的复制偏移量。主节点检查runid和offset决定返回FULLRESYNC触发全量同步还是CONTINUE增量同步。RunID 在第 5 步中的作用非常关键。当从节点发送PSYNC时携带的runid如果是第一次连接从节点不知道主节点ID会传?。主节点收到?后直接判定为“不认识这个从节点”必然会走全量同步。如果从节点之前已经同步过它会记录主节点的 RunID。断线重连后再次发送PSYNC主节点拿这个 RunID 和自身当前 RunID 做比对一致主节点进一步检查复制偏移量是否落在自己的 Backlog 区间内是则增量同步否则全量同步。不一致主从之间的历史关联已不复存在主节点直接发起全量同步。这里有个容易忽略的坑有些运维同学在排查问题时会手动RESTART主节点也不提前通知从节点。主节点重启后 RunID 变了从节点重连时PSYNC携带的旧 RunID 对不上主节点只能触发全量同步。如果主节点数据量大比如 50GB这个全量同步过程会持续很久期间主节点还要同时处理RDB生成的I/O开销和网络传输带宽占用业务侧往往会出现明显的延迟抖动。3.3 主节点重启后如何避免全量同步如果你需要重启主节点但又希望从节点不要做全量同步Redis 提供了一个很实用的配置项repl-id和repl-backlog的配合。实际上Redis 在持久化 RDB 文件时会把当前复制状态RunID、Master 复制偏移量一并写入。这样即使主节点正常重启它也能够恢复这些信息——前提是你没有丢失持久化文件。具体来说主节点在恢复时如果 RDB 文件里包含了repl-id和repl-offset那么它可以把这些状态重新加载到内存中。从节点再次发送PSYNC时主节点发现自己恢复出来的 RunID 和从节点记录的 RunID 还是一致的复制偏移量也有对应关系于是可能直接就进入增量同步状态避免了一次沉重的全量同步。所以我在生产环境里除非万不得已不会动不动就去动主节点的持久化文件。配置项的调整、运行参数的修改能通过热变更解决的就优先热变更只有那种必须重启的场景才去重启并且会提前确认持久化配置正确、RDB/AOF 文件完好。4. Backlog 复制积压缓冲区增量同步的命脉4.1 Backlog 到底是什么Backlog 是主节点维护的一块固定大小的环形内存缓冲区专门用于保存最近产生的写命令数据。它的大小由repl-backlog-size配置项指定默认是 1MB。可以这样理解主节点每次收到一条写命令除了执行之外还会把这条命令追加到复制积压缓冲区中。这个缓冲区是环形的当数据写满之后最早的数据会被覆盖。所以它的本质是一个有容量的滑动窗口保存的是“最近一段时间内产生的写命令流”。所以说白了Backlog 的容量决定了主节点可以容忍的“从节点断连时间上限”。如果从节点断连期间主节点产生的写命令总量没有超过 Backlog 大小那么从节点重连后就能通过增量同步追上进度。反过来如果断连期间的写数据量超过了 Backlog 大小最早的复制数据就被覆盖了那么从节点重连后发现自己需要的偏移量已经不在缓冲区里了主节点只能无奈地选择全量同步。4.2 复制偏移量offset的计算机制复制偏移量是 Redis 主从复制中衡量进度一致性的核心指标。主节点和从节点各自维护一个 offset主节点每发送一个字节的写命令流给从节点它的 offset 就递增相应的字节数。从节点每接收并执行一个字节的写命令流它的 offset 也递增相应的字节数。正常情况下主从节点的 offset 保持一致或者相差很小。如果从节点因为网络问题暂时断连它的 offset 就会落后于主节点。重连时从节点带着自己的 offset 向主节点请求增量数据主节点判断这个 offset 是否还在 Backlog 的存储范围内。这里有一个最常见的误判案例主节点写入量非常大比如每秒产生几百KB的写命令而repl-backlog-size只有默认的 1MB。那么这个 Backlog 的窗口时间可能只有几秒钟。从节点只要断连超过几秒重连时就极大概率触发全量同步。我在线上排查类似问题时第一步就是看监控面板里主节点的repl_backlog_active和从节点的master_repl_offset两个指标。通过对比这两个值能快速判断出增量同步失败是不是因为 Backlog 太小。一般我会建议将 Backlog 调大到 64MB~128MB 甚至更高但前提是主节点的内存要扛得住这个额外开销。4.3 Backlog 参数调优的实操建议很多运维同学不喜欢动repl-backlog-size因为默认值 1MB 用得好好的何必去改但现实是默认值在绝大多数有一定写入量的业务场景里都不够用。调优时我一般会这么算统计业务高峰期 Redis 每秒写入的命令量包括命令本身的字节数和参数大小。统计从节点可能断连的最长时间。比如网络故障导致的断连一般恢复时间在几十秒到几分钟。估算公式BacklogSize ≈ 每秒写入字节数 × 断连恢复最大时长 × 1.5安全余量。举个例子一个业务高峰期每秒产生 200KB 左右的写命令从节点断连恢复最长可能需要 5 分钟那么BacklogSize至少应该设置为200KB × 300s 60MB再乘上安全系数 1.5就是 90MB。考虑到内存成本和风险平衡我个人会直接设成 128MB省心。注意repl-backlog-size设置得过大也不是没有代价的。Backlog 是常驻内存的缓冲区如果你把三个主节点各自配了 256MB 的 Backlog内存成本也不容忽视。当然这比起全量同步带来的CPU飙升、网络拥堵、业务抖动来说还是划算得多。4.4 查看和监控 Backlog 使用状态现场应急的时候我们可以通过以下命令快速查看当前复制的健康状况127.0.0.1:6379 INFO replication输出里主要关注这几个字段role当前节点角色master/slave。connected_slaves已连接的从节点数量。master_replid主节点当前 RunID。master_repl_offset主节点当前复制偏移量。repl_backlog_sizeBacklog 配置大小。repl_backlog_activeBacklog 是否启用。repl_backlog_first_byte_offsetBacklog 中最早一字节数据的偏移量。repl_backlog_histlenBacklog 中实际存储的数据字节数。如果master_repl_offset与repl_backlog_first_byte_offset之间的差值接近repl_backlog_size说明 Backlog 很快会被写满断连的从节点重连后大概率触发全量同步。5. PSYNC 协议演进从 SYNC 到 PSYNC 再到 PSYNC25.1 老古董 SYNC一次断连就全量在很早的 Redis 版本里只有SYNC命令。从节点每次请求同步主节点都会执行一次全量同步——生成RDB、传输、加载。这个设计在今天看来非常粗暴因为一旦从节点断线重连哪怕只是断了 1 秒也要全量重来一遍。要知道 RDB 生成是 CPU 和内存双密集操作在数据量大时会显著影响主节点性能。所以后来的版本引入了PSYNC实现了部分重同步。这也是现代 Redis 主从复制能稳定工作的关键机制。5.2 PSYNC 的工作流程与关键参数PSYNC的核心优势在于从节点可以带着自己的复制偏移量请求增量数据。如果主节点的 Backlog 中仍然包含从节点需要的复制数据就回复CONTINUE然后开始从对应偏移量开始发送积压数据。如果 Backlog 中已经不包含对应的数据主节点回复FULLRESYNC并附上新的 RunID 和偏移量从节点清空旧数据进入全量同步。从节点的重连逻辑发生在replconf阶段之后具体是一个简短的时序从节点重连主节点完成 TCP 连接。从节点发送REPLCONF相关命令把自己伪装成一个新连接。从节点发送PSYNC master_replid offset开始协商同步方式。主节点根据 RunID 和 offset 判断返回CONTINUE或FULLRESYNC。这里有一个细节值得注意从节点发出的offset是它自己记录的slave_repl_offset这个值可能因为从节点在主节点发生故障转移后的角色切换而变得不准确。因此在某些情况下从节点会发送PSYNC ? -1强制主节点进行全量同步。这在从节点本身状态不完整时是合理的行为。5.3 PSYNC2 解决了什么问题Redis 4.0 引入了 PSYNC2它的核心改进是支持“主从角色互换后的部分重同步”。什么意思呢考虑这样一个场景主节点 A 和从节点 B 保持同步。A 发生故障哨兵把 B 提升为新主节点。原先的从节点 C 本来和 A、B 都在同一个复制拓扑中A 故障后 C 需要重新同步到 B。如果没有 PSYNC2C 的 RunID 记忆和 B 的 RunID 不一致C 只能做全量同步。有了 PSYNC2Redis 会记录“复制历史”中的 RunID 与偏移量之间的关系B 可能识别出 C 请求的偏移量正好落在自己的复制历史中于是直接做增量同步。这个设计对故障转移场景非常实用——它大幅减少了主从切换后的全量同步次数缩短了恢复时间。实际生产里每次主从切换如果都触发全量同步对整体可用性影响极大PSYNC2 是这一痛点的重要缓解手段。6. 主从复制的完整流程拆解从握手到稳定复制6.1 建立连接的握手阶段把主从复制的过程串联起来看初始同步主要分为三个阶段连接建立、全量同步、命令传播。连接建立阶段从节点设置主节点的信息后尝试建立 socket 连接。连接成功后从节点会以模拟客户端的身份发送一系列命令完成握手PING REPLCONF listening-port port REPLCONF capa eof capa psync2 PSYNC replid offsetPING 的目的很简单——验证主节点活着且网络通畅。如果主节点响应超时从节点会不断重试直到建立连接。REPLCONF 阶段主要是把从节点自己的端口号告诉主节点方便主节点执行INFO replication时展示从节点信息。我遇到过一种情况主从之间跨机房部署防火墙只开放了主节点的 6379 端口但没有开放从节点的监听端口。TCP 连接能建立但 PING、REPLCONF 之后可能会莫名其妙地中断因为主节点需要回连从节点端口做某些数据交换。最终排查发现是防火墙把回连的端口拦了。这类网络层面的问题排查起来最耗时但也最基础。6.2 全量同步的内部过程全量同步是整个复制流程中开销最大的阶段主节点收到PSYNC请求判定需要全量同步。主节点回复FULLRESYNC replid offset把当前 RunID 和偏移量发给从节点。主节点在后台触发bgsave生成 RDB 快照文件。RDB 生成期间主节点仍然持续接收写命令并将写命令缓冲到复制客户端缓冲区中。RDB 生成完成后主节点将 RDB 文件发送给从节点。从节点清空自己的旧数据加载 RDB 文件。RDB 加载完成后主节点把缓冲期间积压的写命令发送给从节点。从节点执行这些命令最终和主节点的数据保持一致。整个过程中有一个容易踩的坑主节点向从节点发送 RDB 文件时如果 RDB 文件很大比如 20GB网络传输时间会很长。而网络传输 RDB 并不是一个流的——“全量同步期间的写命令缓冲”会一直堆积在主节点的输出缓冲区里。如果写命令非常多输出缓冲区可能会爆掉触发主节点的保护机制直接断开与从节点的连接最终同步失败。针对这个问题Redis 提供了client-output-buffer-limit slave 256mb 64mb 60这样的配置适当调大从节点的输出缓冲区限制。但这个值不宜无脑调大否则主节点内存会被大量积压的写命令撑爆。6.3 命令传播与心跳机制全量同步完成之后主从之间的关系就进入了命令传播阶段。主节点每执行一条写命令除了写入数据本身还会把命令广播给所有从节点。从节点顺序执行这些命令维持数据与主节点一致。另外主从之间还有心跳机制主节点每隔 10 秒向从节点发送PING探测从节点是否存活。从节点默认每隔 1 秒向主节点发送REPLCONF ACK offset上报自己的复制偏移量。心跳机制的价值在于主节点通过从节点上报的 offset可以感知到从节点的复制延迟。如果某个从节点的延迟超过了配置的阈值如min-replicas-max-lag 10主节点可以配合min-replicas-to-write拒绝写请求避免数据不一致扩大化。7. 常见问题与排查技巧实录7.1 从节点断线重连后频繁全量同步这是最常见的问题也是开头提到的那个事故的主要场景。排查步骤我通常是这样走的先看主节点INFO replication中的repl_backlog_first_byte_offset和master_repl_offset确认 Backlog 是否已被写满覆盖。看从节点的master_repl_offset落后主节点多少。如果差值接近 Backlog 大小那问题基本就定位了。调大repl-backlog-size观察后续是否还有全量同步触发。调参时直接在命令里执行127.0.0.1:6379 CONFIG SET repl-backlog-size 128mb这个操作是热生效的但为了持久化记得同步修改配置文件。否则主节点重启后参数会恢复到默认值。7.2 从节点数据与主节点不一致从节点数据不一致一般有两种可能复制延迟导致从节点暂时落后属于正常现象等追平即可。程序逻辑问题比如业务侧直接连到从节点写了数据或者有人在从节点上执行了非只读命令。排查时可以用INFO replication对比主从的 offset。如果从节点的延迟长期存在且延迟量有增大趋势优先怀疑网络带宽瓶颈或从节点自身性能问题。另外从节点尽量开启slave-read-only yes保证它不会接收到外部写请求。7.3 主从同步延迟较大命令传播阶段的主从延迟主要受网络往返时间影响。跨地域部署时延迟会比较明显。解决思路无非是尽量避免主从跨地域部署实在避免不了就考虑使用基于本地数据中心的同城主从结构。如果业务对一致性要求较高可以使用 Redis 的WAIT命令阻塞等待从节点确认但需要注意WAIT会阻塞主节点后续命令的处理对性能有影响。7.4 从节点一直处于 SYNC 状态排除网络和配置问题后一个容易忽略的原因是repl-diskless-sync配置不当。Redis 支持 diskless 全量同步即不通过 RDB 文件落盘而是直接在内存中生成快照流发送给从节点。如果repl-diskless-sync-delay设置过大主节点可能会等待多个从节点同时请求全量同步后才统一开始导致部分从节点一直停留在等待状态。排查时可以看主节点的日志是否有Waiting for bgthread to start之类的信息。调小repl-diskless-sync-delay或改为repl-diskless-sync no一般就能解决。7.5 主节点内存中 Backlog 占用过高如果确认 Backlog 使用率高但业务写入量并没有那么大可以检查是否有多个从节点在同一时间请求全量同步。一个从节点全量同步时其他从节点如果也跟着断线重连可能会各自触发全量同步导致主节点同时承担多份 RDB 生成和网络传输压力。此时可以设置repl-timeout和合理的从节点重连策略避免同步风暴。8. 主从复制在生产环境中的最佳实践与踩坑记录8.1 配置清单参考根据我维护生产环境的经验一份合理的主从复制核心配置大致是这样的配置项推荐值说明repl-backlog-size64mb~128mb根据写入量、断连时长计算repl-backlog-ttl3600Backlog 释放前的等待时间repl-timeout60主从连接超时时间避免误判断连repl-diskless-syncyes开启无盘同步提高全量同步效率repl-diskless-sync-delay5等待多从节点同时同步的窗口时间slave-read-onlyyes强制从节点只读防止数据被外部误写client-output-buffer-limit slave256mb 64mb 60防止全量同步期间输出缓冲区溢出min-replicas-to-write1主节点写保护至少保持一个健康从节点min-replicas-max-lag10从节点最大允许延迟秒数这组配置不是万能的需要根据业务实际情况调整。比如写入量特别大的业务Backlog 可能需要 256MB 以上而数据量很小、写入不频繁的业务默认的 1MB 也未必是坑。8.2 主从切换操作的正确姿势主从切换是 Redis 运维的高风险操作稍有不慎就会导致数据丢失或者长时间不可用。我总结的稳妥步骤提前备份主节点持久化文件。检查所有从节点的复制延迟确保延迟在可接受范围内。在主节点上执行CONFIG SET slave-read-only no如果是提升某个从节点为新主节点。将从节点提升为主节点执行SLAVEOF NO ONE。将其他从节点重新指向新的主节点。更新原主节点为新主节点的从节点或下线原主节点。整个切换过程中最需要关注的是新主节点的数据完整性和其他从节点的复制起点。如果切换前从节点复制延迟过高切换后会有部分数据丢失业务侧必须有相应的补偿机制。8.3 数据同步延迟的监控指标监控是运维的眼睛。主从复制相关的监控指标我重点看这几个master_repl_offset主节点当前写入进度。slave_repl_offset从节点当前复制进度。master_link_status主从连接状态up表示正常down表示断连。slave_lag从节点秒级延迟。搭建告警时我会把主从连接断开的告警级别设为 P1 级即需要立即处理的严重告警。对于slave_lag超过一定阈值的从节点设立 P2 级告警先监控观察持续上升再升级处理。9. 个人实践经验总结在多次排查 Redis 主从复制问题之后我最大的体会是很多所谓“诡异”的现象本质上都是对基础机制理解不透彻导致的。比如 RunID 变化引发全量同步Backlog 太小引发全量同步这些只要我们提前把这些机制吃透设计之初就把参数配置好绝大多数事故根本不会发生。另外我个人的习惯是每配置好一套主从复制都会在测试环境做一次从节点断连与重连演练手动杀进程、模拟网络抖动观察重连后是走增量还是全量。这种演练虽然费时间但远比线上出事后再慌忙排查来得踏实。还有一个容易被忽略的点主从复制的日志分析。Redis 主节点的日志里其实记录了完整的同步事件比如Partial resynchronization request from xxx accepted. Sending backlog增量同步成功和Full resync requested by slave全量同步开始。生产环境务必开启足够级别的日志并且做好日志归档出了事故才好回溯。主从复制是 Redis 高可用的根基把这个根基打牢了后面做哨兵、集群、故障转移都会省心很多。希望这篇文章能把你在 Redis 主从复制上的“最后一公里”打通至少下次再遇到全量同步风暴时你能第一时间想到去检查 RunID 和 Backlog而不是一脸懵地重启主节点。