☰
Redis主从复制原理与高可用实践:从PSYNC到读写分离架构
2026/9/30 7:36:50 网站建设 项目流程

说起 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 和 offset

Redis 从 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:1

master_link_status 必须显示 up,同步状态正常时还会在从节点看到 master_repl_offset 一直在往前增长。主节点那边则是 role:master,connected_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 个哨兵配 quorum=2,避免误判;二是新的主节点不是随便选的,优先级高的从节点优先,偏移量大的从节点更接近最新状态,这也是为什么前文强调 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 replication,master_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 Cluster

7.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 的主从状态没有那么多玄学,很多故障都是配置变更引发的,有一份快照对照,定位问题的速度能快不少。主从复制这套架构,基础但永不过时,先用熟它,后面再谈复杂度。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询