Redis主从复制原理与实战:从全量同步到增量复制的完整链路
Redis 的主从复制几乎是每个后端开发在从单机走向多机架构时绕不开的第一道坎。很多人第一次接触它是因为线上读请求把单实例压得喘不过气想加几个从节点分担查询也有人是为了做数据冗余防止主节点磁盘挂掉之后数据全丢。但真正把主从复制跑起来之后问题才刚开始从节点数据怎么是空的主从延迟为什么忽高忽低主节点重启之后从节点为什么连不上了这些坑我在实际项目里几乎踩了个遍。这篇就把 Redis 主从复制从建立连接到数据同步、从全量复制到增量复制的完整链路拆开讲清楚顺带把那些文档里一笔带过、实际却最容易出问题的地方补上。不管你是刚接触 Redis 的新手还是已经用过但从没深究过原理的老手看完应该都能对这套机制有个通透的理解。1. 为什么单机 Redis 迟早要面对复制这件事1.1 单实例的三个硬伤Redis 单实例跑起来确实爽部署简单、没有协调开销、延迟低。但只要业务量稍微上来一点三个问题就会同时冒头。第一个是读性能瓶颈。Redis 是单线程处理命令的这里说的是命令执行主线程所有读写请求都排在一个队列里。当 QPS 涨到几万甚至十几万单实例的 CPU 会先扛不住。注意瓶颈往往不在网络而在命令执行本身。这时候最直接的思路就是把读请求分散出去写请求还是走主节点读请求分给多个从节点。第二个是数据可靠性。单实例的 Redis 数据全在内存里虽然开了 RDB 和 AOF 能持久化到磁盘但磁盘坏了、机器宕了恢复期间服务就是不可用的。主从复制能让数据实时同步到另一台机器主节点挂了从节点至少还有一份完整数据。第三个是故障恢复能力。单实例挂了就是彻底挂了没有任何回旋余地。有了从节点配合哨兵或者集群模式才能做到自动故障转移。这三个需求指向同一个技术手段主从复制。它的本质就是把一个 Redis 实例主节点的数据持续地同步到一个或多个 Redis 实例从节点上。1.2 主从复制到底解决了什么又没解决什么先把边界划清楚免得用错地方。主从复制解决的是数据冗余备份、读请求水平扩展、为故障转移提供基础。主从复制不解决的是写性能扩展写还是只能在主节点、自动故障转移这需要哨兵或集群、数据一致性保证它是异步复制主节点写完就返回不保证从节点已经收到。这里有个特别容易误解的点很多人以为主从复制是强一致的主节点写成功从节点就一定有了。不是的。Redis 主从复制默认是异步的主节点执行完写命令把命令发给从节点之后就返回客户端了根本不等从节点确认。这意味着主节点刚写完就宕机这条数据可能还没传到从节点切换之后数据就丢了。理解这一点后面很多诡异现象就都能解释了。提示如果你的业务对数据丢失零容忍光靠主从复制是不够的需要配合 WAIT 命令做同步等待或者考虑其他方案。但 WAIT 会牺牲性能要权衡。2. 主从复制的建立从节点是怎么认主的2.1 三种建立复制关系的方式让一个节点变成从节点有三种常见做法实际项目里各有适用场景。第一种是配置文件方式。在从节点的 redis.conf 里加上一行replicaof masterip masterport老版本叫slaveof。这种方式适合固定拓扑节点重启后会自动重新建立复制关系生产环境最常用。第二种是启动命令参数方式。启动时加--replicaof masterip masterport效果和配置文件一样适合临时测试或者容器化部署时通过环境变量注入。第三种是运行时命令方式。连上从节点后执行REPLICAOF masterip masterport动态建立复制关系。这种方式不用重启适合临时调整拓扑。想断开复制关系就执行REPLICAOF NO ONE从节点会升格为主节点但注意它不会丢弃已经同步的数据。# 运行时建立复制关系 redis-cli -h 192.168.1.10 -p 6379 127.0.0.1:6379 REPLICAOF 192.168.1.20 6379 OK # 查看复制状态 127.0.0.1:6379 INFO replication2.2 建立连接时到底发生了什么从节点执行 REPLICAOF 之后并不是立刻就开始传数据中间有一串握手动作。理解这个过程排查问题时才知道卡在哪一步。从节点会先跟主节点建立 TCP 连接然后发送一个 PING 命令确认主节点能正常响应。主节点回复 PONG 之后从节点发送 AUTH如果主节点配了密码做认证。认证通过从节点发送自己的监听端口信息主节点记录下这个从节点的地址。接下来从节点会发送PSYNC命令这是整个复制流程的核心。PSYNC 带上两个参数一个是主节点的 runid运行 ID一个是复制偏移量 offset。如果是第一次连接从节点不知道主节点 runid就发PSYNC ? -1表示我不知道你是谁也没有历史数据请给我全量同步。主节点收到 PSYNC 后要做一个判断这个从节点是要全量同步还是增量同步判断依据就是 runid 和 offset 能不能对上。对不上就走全量对得上就走增量。这个判断逻辑是后面第 4 节的重点。注意老版本 Redis2.8 之前用的是 SYNC 命令只支持全量复制每次断线重连都要全量传一遍效率极低。现在用的都是 PSYNC支持增量。如果你看到某些老资料还在讲 SYNC知道那是历史即可。2.3 一个容易被忽略的细节从节点默认是只读的从节点建立复制关系后默认配置replica-read-only yes也就是只读。这是合理的因为从节点的数据是从主节点同步过来的如果允许写主从数据就会不一致。但实际项目里有人会想往从节点写数据比如做一些临时缓存。这时候要么改配置成可写强烈不建议会破坏一致性要么就老老实实把写请求都发到主节点。还有个坑从节点只读是针对普通客户端连接的主节点通过复制连接发过来的写命令不受这个限制。所以从节点的数据变化只有两个来源主节点同步过来的以及从节点自己内部的过期删除等操作。3. 全量复制第一次同步为什么要传一整个 RDB3.1 全量复制的完整链路从节点第一次连上主节点或者断线太久无法增量同步时走的就是全量复制。这个过程比想象中复杂涉及多个步骤和一次 RDB 文件的传输。主节点收到全量同步请求后会执行BGSAVE在后台生成 RDB 文件。注意是后台生成主节点不会阻塞还能继续处理客户端请求。但这里有个关键点BGSAVE 期间新来的写命令怎么办如果不管这些写命令就不会出现在 RDB 里从节点就丢数据了。Redis 的做法是在内存里维护一个replication buffer复制缓冲区BGSAVE 开始之后所有新的写命令都追加到这个缓冲区里。等 RDB 生成完主节点先把 RDB 文件发给从节点从节点清空自己的旧数据、加载 RDB。加载完之后主节点再把 replication buffer 里积累的写命令发给从节点。这样从节点的数据就能追上主节点的最新状态。整个链路可以概括成从节点发送PSYNC ? -1主节点执行BGSAVE生成 RDB同时把新写命令写入 replication buffer主节点发送 RDB 文件给从节点从节点清空旧数据加载 RDB主节点发送 replication buffer 中的写命令之后进入命令传播阶段主节点每收到一个写命令就实时发给从节点3.2 全量复制的开销到底有多大全量复制是重操作开销体现在三个地方。主节点的 fork 开销。BGSAVE 会 fork 一个子进程fork 本身在数据量大时会有明显的耗时。虽然 Linux 的 fork 用了写时复制Copy-On-Write但 fork 那一刻要复制页表内存越大页表越大耗时越长。实测中一个几十 GB 的实例 fork 一次可能要几百毫秒甚至更久这期间主线程是阻塞的。RDB 文件的磁盘和网络开销。RDB 要写到磁盘再读出来发送如果磁盘 IO 慢这一步就是瓶颈。网络传输更是大头一个 10GB 的 RDB 在千兆网卡上光传输就要一分多钟。从节点加载 RDB 的开销。从节点收到 RDB 后要先清空自己的数据FLUSHALL再加载。加载期间从节点是阻塞的无法响应客户端请求。如果从节点还在对外提供读服务这段时间读请求会全部失败或超时。提示全量复制期间从节点会阻塞所以生产环境要避免在业务高峰期做全量同步。可以通过replicaof的时机控制或者用repl-diskless-sync让主节点直接把 RDB 通过网络发给从节点省掉磁盘这一步。3.3 无盘复制省掉磁盘这一步Redis 2.8.18 之后支持无盘复制配置repl-diskless-sync yes。开启后主节点不再把 RDB 写到磁盘而是 fork 出子进程后直接通过网络把 RDB 数据发给从节点。这个模式适合磁盘 IO 是瓶颈、但网络带宽充足的场景。不过它也有代价主节点要等一个repl-diskless-sync-delay默认 5 秒的窗口把这段时间内请求同步的从节点攒一起然后一次性并发发送避免每个从节点都 fork 一次。这个延迟意味着从节点建立复制会慢几秒但能显著减少主节点的 fork 次数。选择有盘还是无盘核心看瓶颈在哪。磁盘快网络慢用有盘磁盘慢网络快用无盘。没有绝对优劣。4. 增量复制断线重连后怎么只补差的那部分4.1 PSYNC 的判断逻辑增量复制是 PSYNC 相比老 SYNC 最大的进步。它的核心是让从节点断线重连后只同步断开期间丢失的那部分数据而不是全量重来。判断能不能增量靠三个东西runid、offset、repl_backlog。runid 是每个 Redis 实例启动时生成的唯一标识。从节点会记住主节点的 runid。如果从节点重连时发现主节点的 runid 变了说明主节点重启过或者换了实例那就没法增量只能全量。offset 是复制偏移量。主节点每发送 N 字节数据自己的 offset 就加 N从节点每收到 N 字节自己的 offset 也加 N。正常情况下两者应该一致。从节点重连时会带上自己的 offset主节点看这个 offset 之后的数据还在不在自己的 backlog 里。repl_backlog 是主节点维护的一个环形缓冲区默认 1MB配置项repl-backlog-size。主节点发送给从节点的命令除了发出去还会写一份到这个 backlog 里。如果从节点的 offset 落在 backlog 范围内主节点就能把 offset 之后的数据补发给它走增量如果 offset 太旧已经被 backlog 覆盖了那就只能全量。4.2 repl_backlog 的大小怎么定backlog 太小是增量复制失败最常见的原因。默认 1MB 在高写入场景下可能几秒钟就被写满了从节点只要断线超过这个时间重连就得全量。backlog 该设多大有个粗略的估算公式backlog_size 平均写入速率(字节/秒) × 预计最长断线时间(秒)比如你的主节点平均每秒写入 500KB你希望从节点断线 60 秒内都能增量恢复那 backlog 至少要 500KB × 60 30MB。实际配置时再留点余量设成 64MB 比较稳妥。# 动态调整 backlog 大小重启后失效要写进配置文件 redis-cli CONFIG SET repl-backlog-size 64mb注意backlog 是所有从节点共享的不是每个从节点一份。所以从节点越多、写入越频繁backlog 被覆盖得越快。高写入 多从节点的场景backlog 要设得更大。4.3 增量复制的数据流增量复制的过程相对简单从节点重连发送PSYNC runid offset主节点检查 runid 是否匹配、offset 是否在 backlog 范围内都满足主节点回复CONTINUE表示可以增量主节点把 backlog 中 offset 之后的数据发给从节点之后进入正常的命令传播阶段如果 runid 不匹配或 offset 太旧主节点回复FULLRESYNC runid offset从节点就知道要走全量了。这里有个细节主节点回复CONTINUE之后会立刻把 backlog 里 offset 之后的数据发过去然后转入实时命令传播。从节点收到这些数据后更新自己的 offset之后就跟主节点保持同步了。5. 命令传播同步建立之后主从怎么保持同步5.1 实时传播的机制全量或增量同步完成后主从进入稳定状态也就是命令传播阶段。这个阶段主节点每执行一个写命令就会把这个命令准确说是命令的协议格式发给所有从节点。注意主节点是把写命令本身发给从节点而不是发数据快照。从节点收到命令后自己执行一遍。这样做的好处是传输量小一个SET key value命令就几十字节比传整个 value 的数据结构高效得多。但这也带来一个要求从节点执行命令的结果必须和主节点一致。所以 Redis 要求主从的版本尽量一致某些命令在不同版本上行为可能不同。另外像SPOP、SRANDMEMBER这种带随机性的命令主节点会把结果确定化之后再传播保证从节点执行结果一致。5.2 心跳检测怎么知道对方还活着命令传播是单向的主到从但主从之间还需要双向的心跳来确认对方状态。从节点会每秒向主节点发送REPLCONF ACK offset带上自己当前的复制偏移量。这个心跳有三个作用一是让主节点知道从节点还活着二是让主节点知道从节点同步到哪了三是用于min-replicas-to-write这类配置的判断。主节点也会定期默认每 10 秒向从节点发送 PING确认从节点响应正常。如果从节点长时间没响应主节点会把它标记为下线。# 在主节点查看从节点状态 127.0.0.1:6379 INFO replication # 输出里会列出每个从节点的 ip、port、state、offset、lag 等信息其中lag字段很关键它表示从节点最后一次向主节点发送 ACK 距今多少秒。lag 持续大于 1 秒说明从节点可能有问题或者网络有延迟。5.3 主从延迟是怎么产生的主从延迟是异步复制的必然产物但延迟忽大忽小就有问题了。常见的延迟来源有几个。网络往返。主节点发命令到从节点收到中间有网络传输时间。同机房通常 1ms 以内跨机房可能几十毫秒。从节点负载。从节点如果还在对外提供读服务读请求会占用 CPU导致复制命令的处理被推迟。从节点越忙延迟越大。大命令阻塞。如果主节点执行了一个DEL大 key 或者FLUSHALL这个命令传到从节点后从节点执行也要花时间期间无法处理其他复制命令延迟就上来了。主节点写入过快。主节点写入速率超过从节点处理速率命令在从节点的接收缓冲区里堆积延迟持续增长。排查延迟先看INFO replication里的 offset 差值再看从节点的 CPU 和网络。如果 offset 差持续扩大基本就是从节点处理不过来。6. 那些文档不会告诉你的踩坑实录6.1 从节点数据是空的全量复制没走完第一次配主从最容易遇到的现象是从节点INFO replication显示master_link_status:down或者master_link_status:up但keyspace是空的。这种情况八成是全量复制没走完。可能的原因RDB 文件太大传输超时、从节点磁盘空间不够加载失败、主节点 BGSAVE 失败比如stop-writes-on-bgsave-error触发。排查顺序先看主节点日志有没有 BGSAVE 失败再看从节点日志有没有加载 RDB 报错最后看网络传输是否正常。我遇到过一次是从节点磁盘满了RDB 下载到一半失败日志里只有一行不起眼的错误找了半天。6.2 主节点重启后从节点疯狂全量同步主节点重启后 runid 会变除非配置了repl-diskless-load或者用了持久化的 runid从节点发现 runid 对不上就会触发全量同步。如果从节点多、数据量大主节点重启后会被一堆全量同步请求打爆。缓解办法主节点重启前先做一次SAVE或者确保 RDB 是最新的重启后从节点全量同步时能快一点。更好的办法是用哨兵或集群做故障转移避免主节点直接重启。6.3 从节点只读但数据还是不一致理论上从节点只读数据应该和主节点完全一致。但实际中可能因为过期键的处理出现短暂不一致。Redis 的过期键删除有两种惰性删除访问时检查和定期删除。从节点不会主动删除过期键而是等主节点删除后同步DEL命令过来才删。所以在主节点删除之前从节点上这个键可能已经逻辑过期但还存在。客户端在从节点读到这个键会拿到过期数据。这是设计上的取舍不是 bug。如果业务对过期时间敏感读请求最好还是走主节点或者接受这个短暂的不一致。6.4 复制缓冲区溢出导致连接断开主节点的 replication buffer 是有限的受client-output-buffer-limit replica控制默认 256MB 硬限制、64MB 软限制持续 60 秒。如果从节点处理太慢buffer 堆积超过限制主节点会强制断开这个从节点的连接。断开之后从节点重连如果 backlog 够大还能增量否则又是全量。所以从节点性能要跟上buffer 限制也要根据实际情况调整。# 调整复制缓冲区限制硬限制 512MB软限制 128MB 持续 60 秒 redis-cli CONFIG SET client-output-buffer-limit replica 512mb 128mb 607. 主从复制的参数调优与监控要点7.1 关键配置项一览配置项默认值作用调优建议replicaof无指定主节点生产用配置文件方式replica-read-onlyyes从节点只读保持 yes别改repl-backlog-size1mb复制积压缓冲区大小按写入速率和断线容忍时间估算建议 64mb 起repl-backlog-ttl3600backlog 无从节点后保留时间从节点频繁断连可调大repl-diskless-syncno无盘复制磁盘慢网络快时开启repl-diskless-sync-delay5无盘复制等待窗口从节点多可适当调大client-output-buffer-limit replica256mb 64mb 60复制缓冲区限制从节点慢时调大min-replicas-to-write0至少几个从节点在线才允许写对可靠性要求高时设为 1 或 2min-replicas-max-lag10从节点最大延迟秒数配合上一项使用7.2 必须监控的指标主从复制出问题往往不是突然的而是有征兆。几个必须盯的指标master_link_status。up 表示连接正常down 表示断开。这个要配告警。master_repl_offset 与 slave_repl_offset 的差值。差值持续扩大说明从节点跟不上延迟在累积。master_last_io_seconds_ago。从节点最后一次和主节点通信距今秒数超过 10 秒要警惕。master_sync_in_progress。是否正在全量同步为 1 说明正在同步此时从节点不可用。# 一条命令快速检查复制健康度 redis-cli INFO replication | grep -E role|master_link_status|master_last_io_seconds_ago|master_sync_in_progress|slave_repl_offset7.3 一个实用的健康检查脚本思路生产环境建议写个定时脚本定期检查主从状态异常时告警。核心逻辑连上每个从节点读INFO replication检查master_link_status是否为 up检查master_last_io_seconds_ago是否小于阈值比如 10 秒检查master_sync_in_progress是否为 0连上主节点对比主从 offset 差值是否在可接受范围任一异常就触发告警这个脚本用 Python 的 redis 库几行就能写出来比等到业务报错再排查主动得多。8. 从主从复制到高可用下一步该往哪走主从复制本身不提供自动故障转移。主节点挂了从节点不会自动顶上需要人工介入或者借助外部组件。哨兵模式是在主从复制基础上加了一层监控和自动切换。哨兵集群监控主从节点主节点挂了之后自动选一个从节点升为主节点并通知其他从节点切换过去。适合中小规模、对自动故障转移有要求的场景。集群模式则是分片 复制的组合每个分片内部还是主从复制分片之间做数据分片。适合数据量大、需要水平扩展的场景。不管往哪个方向走主从复制都是基础。把这一层理解透了哨兵的选主逻辑、集群的故障转移本质上都是在这套复制机制上做文章。我在实际项目里的体会是主从复制配置本身不难难的是把它跑稳。backlog 大小、缓冲区限制、从节点性能这几个参数要根据实际写入量和拓扑反复调。上线前一定要做断线重连测试和主节点重启测试看看增量复制能不能正常工作别等到真出故障了才发现每次都要全量同步。另外监控比配置更重要主从延迟这种问题早发现早处理拖到业务感知就晚了。