Redis 主从复制:从全量同步到人工故障切换
当一台 Redis 同时承担全部读写和全部数据时,它既是性能入口,也是唯一故障点。增加一个副本看起来只是“多存一份数据”,但真正需要理解的是:数据怎样追上主库、断线后怎样续传,以及主库故障时为什么副本不会自动接管写入。
Redis 主从复制解决的是数据副本问题。它为 Sentinel 和 Redis Cluster 的故障转移提供基础,但它自身不负责自动选主。
🎯 主从复制解决什么
一个最简单的拓扑只有两个角色:
应用写入
│
▼
主库 192.168.10.11:6379
│ 异步复制流
▼
副本 192.168.10.12:6379
主库处理写命令,副本接收并重放复制流。副本默认只读,可以用于数据冗余、只读查询或备份来源。
它不解决以下问题:
- 主库故障后不会自动提升副本。
- 应用不会自动知道写入口已经变化。
- 异步复制不能承诺故障时零数据丢失。
- 副本不是备份,误删除和错误写入同样会被复制。
因此,“有副本”和“服务高可用”是两件事。
🔄 Redis 如何保持副本同步
理解 Redis 复制,先记住三个关键要素:复制 ID、复制偏移量和复制积压缓冲区。
复制 ID
主库维护一个 replication ID,用来标识当前复制历史。副本记录自己跟随的复制 ID。两端 ID 一致,才有可能在断线后沿用原来的复制历史。
发生主从切换后,复制历史可能改变。Redis 还会保留上一代复制 ID 和对应偏移量,让刚完成角色切换的节点有机会继续进行部分重同步。
复制偏移量
主库每向复制流写入一批字节,偏移量就向前推进;副本处理复制流后也会更新自己的偏移量。两者差值可以近似反映副本落后程度。
偏移量不是业务版本号。它描述的是复制流位置,而不是“已经复制了多少个键”。
复制积压缓冲区
主库会保留最近一段复制流。副本短暂断线后,如果需要的数据仍在积压缓冲区内,就不必重新接收完整数据集,只需要续传缺失部分。
缓冲区越大,越能容忍较长的网络抖动,但也会占用更多内存。是否能够部分重同步,取决于复制历史是否一致,以及缺失区间是否仍然保留。
容量可以先按“平均复制流字节数/秒 × 希望容忍的断线秒数 × 安全余量”估算,再通过 repl-backlog-size 调整。例如写入流量约为 2 MB/s,希望容忍 5 分钟中断,理论下限约为 600 MB,实际还要为流量峰值预留空间。
首次连接:全量同步
副本首次连接主库时,没有可复用的复制历史,通常需要全量同步:
- 副本连接主库并发送
PSYNC。 - 主库判断无法续传,返回全量同步响应。
- 主库生成当前数据集的 RDB 快照,同时把后续写命令暂存在缓冲区。快照可以先写入磁盘,也可以通过无盘复制直接发送给副本。
- 副本加载快照。
- 主库发送快照生成期间积累的写命令。
- 两端进入持续命令传播阶段。
主库 副本
│ <──── PSYNC ──────── │
│ ─── FULLRESYNC ────> │
│ ───── RDB 快照 ─────> │
│ ─── 缓冲写命令 ─────> │
│ ─── 持续复制流 ─────> │
全量同步可能触发快照生成、网络传输和副本加载,对 CPU、内存、磁盘和带宽都有影响。大量副本同时进行全量同步,可能反过来拖慢主库。
断线重连:部分重同步
副本重连时会携带复制 ID 和已经处理到的偏移量。主库检查后有两种结果:
- 复制历史一致,而且缺失数据还在积压缓冲区:执行部分重同步,只发送断线期间缺失的复制流。
- 复制历史不一致,或者缺失区间已经被覆盖:退回全量同步。
这就是复制积压缓冲区的主要价值:它不是持久化日志,而是帮助副本跨越短暂连接中断。
⚠️ 异步复制的边界
写入确认与数据丢失窗口
Redis 主库执行写命令后,可以在副本确认之前向客户端返回成功。这样延迟较低,但形成了一个风险窗口:
客户端写入成功
│
├─ 主库已经执行
└─ 副本尚未收到 ── 此时主库故障
如果此时人工提升副本,尚未复制的写入可能丢失。WAIT 可以让某个客户端等待指定数量的副本确认其写入偏移量,但它只覆盖当前客户端连接此前执行的写入,不是全局写入栅栏。它不会把 Redis 复制改造成强一致协议,也不能替代故障域和持久化设计。
副本读流量的代价
把只读请求分配到副本可以降低主库压力,但应用必须接受以下事实:
- 刚写入主库的数据可能暂时无法从副本读到。
- 同一个用户先写后读,如果两次请求落到不同节点,可能看到旧值。
- 主从切换或全量同步期间,副本延迟可能突然升高。
- 增加副本会增加主库的复制连接、带宽和同步成本。
读写分离不是免费的横向扩容。对强依赖“写后立即可读”的路径,通常仍应读取主库,或者在应用层设计一致性策略。
🚧 人工切换的风险
为什么不能只提升副本
计划内切换至少包含四个动作:
- 停止业务向旧主库写入,形成明确的写入栅栏。
- 确认副本已经追上可接受的复制位置。
- 在副本执行
REPLICAOF NO ONE,将其提升为主库。 - 切换应用连接,并把旧主库改成新主库的副本。
只执行第三步会留下两个都能接受写入的节点。客户端如果同时写到两边,就会产生互相无法自动合并的数据分叉。
旧主库故障时可能只能跳过“等待追平”,因此需要接受最近写入丢失的可能。如果只是网络分区,旧主库可能仍在另一侧接受写入;无法确认它已经停止时,必须先隔离旧主库,再提升副本。旧主库恢复后也不能直接放回流量,必须先清除其旧角色并重新跟随当前主库。
🐳 双服务器最小部署
下面用两台 Linux 服务器演示复制链路:
| 服务器 | 地址 | 角色 |
|---|---|---|
| node-1 | 192.168.10.11 | 主库 |
| node-2 | 192.168.10.12 | 副本 |
配置主库与副本
示例使用 redis:8.8.0 和 host 网络。两台服务器分别准备 redis.conf 和 compose.yml,数据写入 Docker 命名卷。示例密码仅用于说明,部署时必须替换。如果改用 ./data:/data 绑定宿主机目录,需要提前确认该目录允许容器内 Redis 进程写入。
node-1 的 redis.conf:
bind 0.0.0.0
protected-mode yes
port 6379
requirepass RedisBlog_ChangeMe_2026!
masterauth RedisBlog_ChangeMe_2026!
appendonly yes
appendfsync everysec
dir /data
daemonize no
node-2 在相同基础上增加复制配置:
replicaof 192.168.10.11 6379
replica-read-only yes
replica-announce-ip 192.168.10.12
replica-announce-port 6379
replica-read-only 不是安全边界,它只能阻止普通客户端误写,不能代替密码、ACL、防火墙和管理命令访问控制。
两台服务器使用相同的 compose.yml:
services:
redis:
image: redis:8.8.0
container_name: redis-replication
restart: unless-stopped
network_mode: host
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
volumes:
- ./redis.conf:/usr/local/etc/redis/redis.conf:ro
- redis-data:/data
volumes:
redis-data:
启动容器
先启动 node-1,再启动 node-2:
docker compose up -d
验证复制关系
在 node-1 查看主库状态:
docker exec -e REDISCLI_AUTH='RedisBlog_ChangeMe_2026!' \
redis-replication redis-cli INFO replication
应重点看到:
role:master
connected_slaves:1
node-2 应显示:
role:slave
master_host:192.168.10.11
master_link_status:up
最后在 node-1 写入、node-2 读取:
# node-1
docker exec -e REDISCLI_AUTH='RedisBlog_ChangeMe_2026!' \
redis-replication redis-cli SET article:replication ok
# node-2
docker exec -e REDISCLI_AUTH='RedisBlog_ChangeMe_2026!' \
redis-replication redis-cli GET article:replication
一次读写成功只证明基本链路可用。长期运行可以重点采集以下指标:
| 指标 | 关注点 |
|---|---|
master_link_status、master_last_io_seconds_ago | 副本与主库的链路是否正常、多久没有收到复制数据 |
master_repl_offset、slave_repl_offset | 主副本偏移量差值,也就是副本大致落后多少复制流字节 |
repl_backlog_active、repl_backlog_size | 积压缓冲区是否启用、容量能否覆盖预期断线时间 |
sync_full、sync_partial_ok、sync_partial_err | 全量同步是否频繁、部分重同步成功率是否下降 |
aof_enabled、aof_last_write_status | AOF 是否启用、最近一次写入是否成功 |
INFO replication 的部分字段仍保留 master、slave 命名,这是 Redis 输出中的兼容字段,不影响正文使用“主库”和“副本”。除了这些指标,还应监控内存、磁盘、网络带宽和容器重启次数。
🔀 计划内人工切换
下面的命令用于计划内维护。执行前先在应用、代理或入口层停止所有写入,避免切换过程中产生新数据。
检查复制位置
先分别查看 node-1 和 node-2 的复制状态:
# node-1:记录主库复制偏移量
docker exec -e REDISCLI_AUTH='RedisBlog_ChangeMe_2026!' \
redis-replication redis-cli --raw INFO replication \
| grep -E 'role|master_repl_offset'
# node-2:确认链路正常,并观察副本处理到的偏移量
docker exec -e REDISCLI_AUTH='RedisBlog_ChangeMe_2026!' \
redis-replication redis-cli --raw INFO replication \
| grep -E 'role|master_link_status|slave_repl_offset'
提升副本与旧主回归
确认 master_link_status:up,并且副本偏移量已经追上旧主库后,在 node-2 提升副本:
# node-2
docker exec -e REDISCLI_AUTH='RedisBlog_ChangeMe_2026!' \
redis-replication redis-cli REPLICAOF NO ONE
随后让 node-1 跟随 node-2。node-1 在完成角色转换前应继续保持写入隔离:
# node-1
docker exec -e REDISCLI_AUTH='RedisBlog_ChangeMe_2026!' \
redis-replication redis-cli REPLICAOF 192.168.10.12 6379
最后确认 node-2 为 role:master、node-1 为 role:slave 且复制链路为 up,再把应用写入口切到 192.168.10.12:6379 并恢复流量。故障接管时如果无法访问旧主库,应先通过网络、安全组、进程或宿主机层面完成隔离,再执行提升命令。
需要自动完成这些判断和动作时,应使用 Redis Sentinel,而不是不断扩充人工切换脚本。
🧭 什么时候选择主从复制
适合使用主从复制的情况:
- 需要一个近实时数据副本,并且可以接受人工恢复。
- 读请求允许短暂读取旧数据。
- 团队希望从最简单的复制拓扑开始。
如果目标是单主写入下的自动故障转移,继续阅读 Redis Sentinel:主库故障后如何完成自动选主。如果数据容量或写吞吐已经超过单个主库,则应评估 Redis Cluster:哈希槽、客户端路由与故障转移。