说起 Redis 主从复制我先交代一个让我印象深刻的场景。有一年线上 Redis 还是单实例部署缓存里放着商品详情和会话数据某天夜里进程突然 OOM 退出登录接口的 session 校验瞬间全部落到数据库数据库连接被打满整条链路雪崩。事后复盘时才发现这个单点问题不是“会不会出”而是“什么时候出”。主从复制就是这句话给出的标准解法——数据高可用和读侧负载均衡的核心方案。它让同一份数据同时存在于主库与多个从库实例上主库出问题时有备用副本可以立刻顶上读请求也可以被分散到多台从库不至于让一台实例独自承担全部压力。这篇文章我会从复制原理、Docker 实操、读写分离路由、哨兵故障转移、常见踩坑一直讲到集群边界把主从复制相关的内容完整过一遍。1. 为什么单机 Redis 扛不住两个最典型的故障场景1.1 进程宕机引发的缓存雪崩如果 Redis 只有一台那么所有缓存数据都在这台机器上。进程挂了之后可能出现两种情况一种是有持久化RDB/AOF重启后能从磁盘恢复但恢复期间所有请求仍然要打向后端另一种是没开持久化重启后缓存里的数据全没了下一次写缓存之前数据库会被密集的读请求连续冲击一段时间这种集中穿透就是缓存雪崩。我见过很多团队在 Redis 配置里只把 RDB 开着但默认的 save 策略可能要几分钟才落一次盘主进程异常退出时最后几分钟的数据直接丢了。很多业务对缓存丢失敏感度没那么高关键问题在于“恢复时间”。一台 Redis 恢复数据至少要几十秒到几分钟这段时间对于在线业务来说已经非常长了。主从复制相当于给数据提前准备了一个热备副本主节点挂了从节点马上能把读请求接走数据也不会丢太多。1.2 读多写少场景下的 CPU 瓶颈第二个典型场景是读压力。缓存类业务绝大多数是读多写少可能一个商品详情页被读上千次才更新一次。单实例 Redis 的写入 QPS 有余量但读请求把所有 CPU 核打满单线程的瓶颈就暴露出来。主从复制最大的价值之一是让多个从实例承担读流量把 QPS 从单机撑到的几万扩到几十万甚至更高。这里要特别说一句实话主从复制只是把读流量分散了写流量还是集中在主节点所以它的“负载均衡”是有边界条件的。如果你的系统写多读少主从复制对写侧几乎没有帮助那就要考虑分片型方案了这一点后面专门展开。1.3 主从复制与“数据高可用、负载均衡”的真实关系把标题里的两个关键词拆开看数据高可用其实要靠“主从复制哨兵”的组合主从复制本身只保证数据有副本不会自动把请求切到副本上负载均衡指的是读流量可以被调度到多个从节点写流量没有平衡。搞清楚这个边界你才能判断自己的场景到底适合怎么搭。这也解答了很多新手的一个疑惑明明配置了主从复制主节点宕机后客户端还是连不上 Redis。原因很简单主从复制只负责数据同步不负责故障转移。要实现自动漂移必须加上 Redis Sentinel 或者业务侧的监控切换逻辑。这不算什么高深的东西但架构认知清晰与否往往就体现在这些边界上。2. 复制协议拆解replid、offset 与 PSYNC 的实际行为2.1 每个实例都有复制身份replid 和 offsetRedis 从 2.8 开始用 PSYNC 命令做复制一句 PSYNC 背后依赖两个关键标识replid复制ID和 offset复制偏移量。主节点有一个唯一 replid从节点成功同步后会记录主节点的 replid。可以理解成replid 表示“这份数据来自哪个主库”offset 表示“我已经同步到了哪一条写命令”。当主节点重启或者从节点重连时从节点会把 replid 和 offset 一起告诉主节点。如果 replid 对得上说明历史数据来源一致offset 就是用来判断主节点还能不能只补增量。在 redis-cli 里执行 info replication主节点大概能看到 master_replid 和 master_repl_offset。从节点会显示 master_replid、master_repl_offset以及 slave_repl_offset。线上排查复制进度的第一步通常就是对比这两个 offset差值过大说明同步滞后明显。2.2 首次全量同步RDB 生成、传输与后续命令补发第一次建立主从关系或者主节点 replid 变化时Redis 会走全量同步。全量同步的过程大致四步主节点 fork 出子进程执行 BGSAVE生成 RDB 快照把 RDB 传给从节点从节点清空旧数据并加载 RDB主节点把 BGSAVE 期间新产生的写命令补发给从节点。这里有两个容易被忽略的细节。第一个是 fork 的影响主节点属于内存密集型的进程fork 时虽然有 Copy-On-Write 兜底但内存越大fork 瞬间阻塞主线程的风险越高。第二个是 RDB 传输时间几 GB 的 RDB 走内网可能很快走公网就很容易超时所以从节点的 timeout 配置要合理网络环境不佳时还需要调整 repl-timeout否则会反复全量同步。Redis 7 之后还能配置无盘复制repl-diskless-sync主节点不落盘 RDB直接把内存里的数据通过 socket 发给从节点适合从节点比较多、磁盘 IO 紧张的场合。2.3 增量同步与 repl_backlog断线重连为什么不必全量重来Redis 主从之间每隔一段时间会相互确认偏移量从节点断线重连后主节点会通过 repl_backlog 这个环形缓冲区判断能不能补增量。只要从节点的 offset 还在 backlog 覆盖范围内主节点直接发送之间的写命令offset 已经被挤出去了就只能退化为全量同步。repl_backlog 默认只有 1MB对于写入量大的业务来说从节点断线几十秒就可能让 offset 飞出 backlog 的区间触发一次开销很大的全量同步。所以先评估平时每秒写入量再把 repl-backlog-size 调成几个 MB 甚至几十 MB是一种很便宜的保险。多从节点共用一个 backlog多个从节点同时断线重连时这个缓冲区更容易被挤出这一点也需要留意。2.4 异步复制的取舍不等待从节点回执的设计逻辑默认情况下Redis 主节点执行客户端写命令后不会等待从节点确认就返回。这是设计上的主动选择保证主节点写入性能不被网络延迟拖累代价是极短时间窗口内主从数据可能不一致。如果业务对一致性敏感可以在主节点配置 min-replicas-to-write 和 min-replicas-max-lag让自己的写入至少在 N 个副本在线时才成功这是一种降低风险的折中。从这里也能理解Redis 的主从复制不是强同步复制它更适合缓存、读多写少的业务。把 Redis 当数据库用又要保证实时一致性的场合主从复制只能作为高可用方案业务侧必须接受存在秒级内的不一致窗口。3. 一主两从的落地实测Docker Compose 一条龙3.1 容器网络里的主机名解析本地验证主从关系最方便的方式是用 Docker Compose 起三个 Redis 容器。同一个 compose 网络内的容器可以用服务名直接解析所以从节点配置 replicaof 时不需要写宿主机 IP直接写 redis-master 6379这是本地实验比手搓三个虚机舒服很多的地方。建议先建一个独立网络比如 redis-demo避免和生产网络冲突。三个容器分别映射宿主机 6379、6380、6381 端口这样本地用客户端工具或者 redis-cli 都能直接连上去看角色。3.2 三个 redis-server 的启动命令与端口映射下面这份 docker-compose.yml 可以直接保存使用services: redis-master: image: redis:7 container_name: redis-master ports: - 6379:6379 command: redis-server --appendonly yes redis-slave1: image: redis:7 container_name: redis-slave1 ports: - 6380:6379 depends_on: - redis-master command: redis-server --replicaof redis-master 6379 --appendonly yes redis-slave2: image: redis:7 container_name: redis-slave2 ports: - 6381:6379 depends_on: - redis-master command: redis-server --replicaof redis-master 6379 --appendonly yes执行 docker compose up -d 后等十几秒三个容器就都起来了。注意端口映射的格式是“宿主机端口:容器端口”容器内部都是 6379从节点只要指定 redis-master:6379 就能连到主节点。3.3 配置认证后必须补的 masterauth生产环境的主节点一般会开启 requirepass这时候从节点光配 replicaof 是不够的必须同步配置 masterauth否则从节点会不停提示认证失败复制状态一直处于 down。不少公司的主从不同步事故排查到最后就是 masterauth 没配上。如果修改密码主节点和所有从节点的 masterauth 都要一起改还要重启或执行 config set 动态刷新。建议把密码放到配置管理里统一管理别只手工改一台主节点否则迟早出乱子。3.4 使用 info replication 验证角色状态进入任意从节点执行 redis-cli info replication关注几个关键字段role:slave master_host:redis-master master_link_status:up master_repl_offset:123 slave_read_only:1master_link_status 必须显示 up同步状态正常时还会在从节点看到 master_repl_offset 一直在往前增长。主节点那边则是 role:masterconnected_slaves:2两个从节点的 lag 字段能看出同步延迟的秒数。这套字段是日常巡检最重要的判断依据没有之一。可视化工具也能看比如用客户端分别连三个端口左侧树里展开节点信息角色、偏移量、连接从节点数一目了然。但命令行的 info replication 依然是精确排查的第一选择。4. 读写分离的负载均衡细节客户端路由才是关键4.1 分清“主从支持读横向扩展”和“系统整体负载均衡”主从复制建好之后从节点并不会自动接收读流量。你的应用代码如果还是连主节点那么从节点再多也只是热备没发挥负载均衡的作用。真正决定负载均衡效果的是客户端的连接池配置和路由策略。以 Java 生态为例Spring Data Redis 默认连的是一个主节点要让读请求打到从节点得给 Lettuce 设置 ReadFrom。对于 Lettuce 客户端常见做法是LettuceClientConfiguration.builder() .readFrom(ReadFrom.REPLICA_PREFERRED) .build();ReadFrom.REPLICA_PREFERRED 表示优先读从节点从节点不可用再读主节点ReadFrom.REPLICA 则强制只读从节点所有从节点都挂时读操作会报错。选哪种取决于你们对“读不可用”的容忍度。如果你是 Python 生态redis-py 的哨兵连接模式下也有专门的从节点路由参数思路一致。4.2 Lettuce 与 Spring Data Redis 的 ReadFrom 路由配置好后读写流量大致是这样分布的写命令和事务命令继续走主节点读命令按负载策略分配到从节点。从节点越多单台承担的压力越低。主节点压力低了整体的 CPU、延迟、连接数才有余量。这里特别提醒一下Lettuce ReadFrom 的控制粒度是全局的如果你的业务里既有强一致读又有弱一致读就需要在代码里做更细的路由控制而不是全靠客户端配置。可以给强一致读的 Repository 单独指定一个直连主节点的配置避免所有流量都被路由到从节点。4.3 从节点只读是默认保命配置replica-read-only从节点默认 replica-read-only yes这时候任何写命令都会被拒绝。可有些同学为了临时调试把从节点设成可写然后在从节点上改了几个 key结果主从一同步从节点上被覆盖的数据直接和主节点冲突后续数据不一致的坑非常难排查。所以除非你明确知道自己在干什么否则从节点永远保持只读。如果真的需要从节点承接临时写入宁可接一层应用逻辑把数据写到主节点也别直接打开从节点的写开关。顺手检查一下配置里的 slave-read-only 或 replica-read-only旧版本字段名不一样容易被忽略。4.4 读延迟造成的业务取舍读写分离之后一定会出现的一个现象刚写入的数据马上读可能读不到因为复制是异步的。对一致性要求高的场景比如支付回调后的余额查询、登录会话校验就不应该路由到从节点。通用的取舍思路是把路由粒度落到业务代码里而不是全局一刀切。比如商品详情页、文章阅读量、二维码配置这类数据延迟几十毫秒完全无感强制走从节点没问题。订单状态、用户余额这种强一致诉求就要在主节点上读。用 Spring 的话可以通过自定义注解或者 ThreadLocal 标记强制读写主库这块属于客户端路由的常规做法。5. 从复制到高可用哨兵如何接管故障转移5.1 主从复制不解决“主节点挂了怎么办”如果只有主从复制主节点宕机后从节点虽然有一份完整数据但客户端连接仍然指向主节点的 IP不会自动切到从节点写服务直接中断。真正把“有副本”变成“故障自动切换”的是 Redis Sentinel也就是哨兵。主从复制管数据同步哨兵管可用性决策两者配在一起才是完整的高可用方案。我遇到过团队把主从复制搭好了就以为高可用完成结果主节点宕机后业务侧等了 20 分钟人工切库。那段等待时间对于在线业务来说就是按分钟计的故障。哨兵的接入成本其实很低但价值非常大。5.2 哨兵的发现、投票与晋升流程哨兵模式下一般部署 3 个哨兵节点。当哨兵连续 down-after-milliseconds 时间没收到主节点 PONG 响应时先判断为主观下线多个哨兵都确认这个状态后达到 quorum 阈值升级为客观下线随后哨兵集群会选举一个 leader 负责执行故障转移从在线从节点中挑一个作为新主。这里注意两个点一是 quorum 不是越大越好建议设置大于哨兵节点数的一半例如 3 个哨兵配 quorum2避免误判二是新的主节点不是随便选的优先级高的从节点优先偏移量大的从节点更接近最新状态这也是为什么前文强调 offset 监控很重要。5.3 原主节点回归后如何变成新从节点故障转移完成后原主节点如果恢复哨兵会把它重新纳入主从架构并让它作为新主节点的从节点replication ID 也随之改变。从节点身份变化时会重新走一次全量同步这是正常行为不是故障。因此生产上看到原主节点恢复后出现一次较大的同步流量不必慌。但这里有一个容易被忽视的问题如果原主节点回归时它的数据已经被新主节点写入覆盖全量同步会以新主节点的数据为准覆盖原主节点上残留的旧数据。所以原主节点不能直接重新接入业务要确保它先完成新的全量同步再开放读流量。5.4 哨兵部署时的建议与坑哨兵配置我喜欢用官方模板核心是三行sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 15000哨兵要能连上主从且每个哨兵的配置保持一致。常见坑包括把哨兵部署在跟 Redis 同台机器机器挂掉时连哨兵一起挂哨兵节点数量为偶数但 quorum 配置不当导致脑裂风险提升哨兵启动后没有及时观察日志导致 failover 触发不了。哨兵本身也需要持久化自己的配置配置文件不能临时用否则重启后信息丢失。6. 踩坑记录三种主从问题的完整排查链路6.1 刚写入的数据读不到不一定是复制断了这是我在生产上遇到最多的一种误判。某天业务反馈说 Redis 主从不一致改了一个 key 之后从节点读不到新值。第一眼去看 info replicationmaster_link_status 是 up两个节点的偏移量也几乎一样说明复制是通的。问题出在客户端路由策略上。应用层的读请求被随机分布到了多个从节点修改 key 的命令走主节点主节点异步把命令同步到从节点从节点 B 收到了但从节点 A 还没来得及处理于是读请求恰好落在 A 时返回旧值。这不是主从坏了而是复制延迟和路由叠加之后的正常结果。排查这类问题要先确定读请求走了哪个节点再看对应节点的偏移量不能一上来怀疑复制断了。6.2 主从反复全量同步积压缓冲区被挤出另一个麻烦的故障形态从节点的日志里反复出现全量同步master 的 CPU 和网络飙升。第一次遇到时我先看 repl-backlog-size默认 1MB再看业务写入量高峰期每秒能产生几百 KB 的写命令从节点断线 10 秒内偏移量就被挤出 backlog 区间于是每次重连都触发全量同步。解决方案并不复杂把 repl-backlog-size 调大到 64MB同时把 repl-backlog-ttl 从 3600 秒调到一个合适值保证在业务低谷期及时释放。改完配置逐字逐句观察主从日志确认从节点只做增量同步问题就消失了。这类问题很典型默认值适合低流量环境不适合生产写入别等到故障现场才想起来调参。6.3 切换过后出现逻辑过期数据源头在删除策略还有一个隐蔽的坑从节点被提升为主节点后长期存在一些“已经过期但物理未删除”的 key。原因是 Redis 的过期 key 删除策略在主从上有特殊约定主节点惰性删除或定期删除某个 key 后会向从节点发送一条 DEL 命令从节点不会主动按时间删除 key而是等主节点命令。平时从节点上这些过期 key 因为惰性检查不会返回给客户端看起来没问题一旦从节点变成主节点缺少了原来主节点下发 DEL 的过程某些过期 key 就可能继续存在直到新主节点的定期删除周期触发。遇到切换后内存异常或者旧值被读到的场景用 scan 检查过期 key 的占比再用合理的淘汰策略兜底就能减轻影响。6.4 我常用的排查手段看信息、看日志、看状态兜底总结一下主从排查的链路。第一步在从节点执行 info replication确认 master_link_status 和两个 offset第二步在主从节点分别看日志里的 sync 相关关键字确认全量还是增量同步第三步看客户端路由配置确认请求是不是真的落到对应节点第四步如果网络层有问题用 ping 和基础连通性工具做验证。过去几年我排查主从问题几乎 80% 都能靠 info replication 定位剩下 20% 属于配置、路由、网络三层叠加的问题需要一层层剥开。为了避免半夜爬起来看日志建议把 info replication 的关键字段做成监控项offset 差距超过阈值就告警master_link_status 变成 down 直接拉故障。7. 主从架构的边界什么时候该上 Redis Cluster7.1 主从冗余与集群分片是完全两码事主从复制让多台实例保存同一份数据这是冗余Redis Cluster 让不同数据 slot 分布在不同分片上这是分片。标题里说的负载均衡在主从层面是“读多实例分担单点压力”在集群层面是“写和内存也能横向扩展”。很多人把这两件事混为一谈结果要么过度设计要么架构欠账。一个典型场景单实例内存已经用到 20GB主从复制再搭几台每台都存 20GB并不解决容量问题。要解决容量只能上集群让每个分片各存一部分数据。同理如果写 QPS 已经超过单实例上限主从复制也不能分担写压力因为写命令始终在主节点执行。7.2 从单机到主从再到集群的演进信号什么时候该从主从升级到集群我整理了几条信号对照自己的业务看信号单机/主从能撑住的阶段需要考虑集群的阶段内存容量单实例内存不超 20~30GB数据总量持续接近单机物理内存写 QPS低于单实例写上限单线程一般十万级写 QPS 逼近瓶颈且无法水平扩展读 QPS通过多从节点可以拖住从节点数量过多同步和运维成本变高扩展性要求节点数少运维简单希望按 slot 平滑扩容缩容如果你已经上了主从还在为读流量发愁最优先的是再增加从节点如果内存或写流量先到瓶颈主从就帮不上忙了该考虑 Redis Cluster。集群内部每个分片依然是主从结构所以主从复制学和集群不是替代关系而是基础组件和上层组织方式的关系。7.3 上集群前先把主从这套功课补齐很多团队在准备上集群时发现最缺的不是集群知识而是对主从复制理解不透。集群分片间同样有主从同步、偏移量、故障迁移这些概念只是搬到了更复杂的拓扑里。如果你连一主两从的 info replication 字段都读不顺直接上集群多半会把问题放大几倍。所以我的建议是先把主从复制做实再做哨兵高可用最后才谈集群。这三层是一层套一层的递进跳过任何一层架构都会留下隐患。最后再分享一个我自己养成的小习惯每次给 Redis 主从做变更比如调密码、改 backlog 大小、加从节点我都会顺手在维护文档里记录变更前后的 info replication 快照。别小看这个习惯Redis 的主从状态没有那么多玄学很多故障都是配置变更引发的有一份快照对照定位问题的速度能快不少。主从复制这套架构基础但永不过时先用熟它后面再谈复杂度。