当一台 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,实际还要为流量峰值预留空间。

首次连接:全量同步

副本首次连接主库时,没有可复用的复制历史,通常需要全量同步:

  1. 副本连接主库并发送 PSYNC
  2. 主库判断无法续传,返回全量同步响应。
  3. 主库生成当前数据集的 RDB 快照,同时把后续写命令暂存在缓冲区。快照可以先写入磁盘,也可以通过无盘复制直接发送给副本。
  4. 副本加载快照。
  5. 主库发送快照生成期间积累的写命令。
  6. 两端进入持续命令传播阶段。
主库                     副本
 │  <──── PSYNC ────────  │
 │  ─── FULLRESYNC ────>  │
 │  ───── RDB 快照 ─────>  │
 │  ─── 缓冲写命令 ─────>  │
 │  ─── 持续复制流 ─────>  │

全量同步可能触发快照生成、网络传输和副本加载,对 CPU、内存、磁盘和带宽都有影响。大量副本同时进行全量同步,可能反过来拖慢主库。

断线重连:部分重同步

副本重连时会携带复制 ID 和已经处理到的偏移量。主库检查后有两种结果:

  • 复制历史一致,而且缺失数据还在积压缓冲区:执行部分重同步,只发送断线期间缺失的复制流。
  • 复制历史不一致,或者缺失区间已经被覆盖:退回全量同步。

这就是复制积压缓冲区的主要价值:它不是持久化日志,而是帮助副本跨越短暂连接中断。

复制偏移量与积压缓冲区主库持续推进复制偏移量。网络断开时副本停止前进,但缺失数据仍保留在主库的复制积压缓冲区中。 副本重新连接并发送 PSYNC 后,主库只续传缺失区间,副本再次追平。REPLICATION OFFSET · BACKLOG WINDOW · PARTIAL RESYNCM主库持续接收写入并推进复制流offset 1280R副本重放复制流,默认只读offset 1230停在 1230追赶中offset 1280PSYNC → CONTINUE主库复制偏移量12001280副本已处理偏移量积压缓冲区仍保留的缺失区间复制流持续传播,副本允许短暂落后网络断开:副本停止,但主库和 backlog 继续前进缺失数据仍在 backlog 中:只续传 1231–1280部分重同步完成,无需重新加载整个数据集
主库偏移量副本已处理位置可部分重同步的积压窗口

⚠️ 异步复制的边界

写入确认与数据丢失窗口

Redis 主库执行写命令后,可以在副本确认之前向客户端返回成功。这样延迟较低,但形成了一个风险窗口:

客户端写入成功

      ├─ 主库已经执行
      └─ 副本尚未收到 ── 此时主库故障

如果此时人工提升副本,尚未复制的写入可能丢失。WAIT 可以让某个客户端等待指定数量的副本确认其写入偏移量,但它只覆盖当前客户端连接此前执行的写入,不是全局写入栅栏。它不会把 Redis 复制改造成强一致协议,也不能替代故障域和持久化设计。

副本读流量的代价

把只读请求分配到副本可以降低主库压力,但应用必须接受以下事实:

  • 刚写入主库的数据可能暂时无法从副本读到。
  • 同一个用户先写后读,如果两次请求落到不同节点,可能看到旧值。
  • 主从切换或全量同步期间,副本延迟可能突然升高。
  • 增加副本会增加主库的复制连接、带宽和同步成本。

读写分离不是免费的横向扩容。对强依赖“写后立即可读”的路径,通常仍应读取主库,或者在应用层设计一致性策略。

🚧 人工切换的风险

为什么不能只提升副本

计划内切换至少包含四个动作:

  1. 停止业务向旧主库写入,形成明确的写入栅栏。
  2. 确认副本已经追上可接受的复制位置。
  3. 在副本执行 REPLICAOF NO ONE,将其提升为主库。
  4. 切换应用连接,并把旧主库改成新主库的副本。
Redis 人工主从切换状态机人工切换依次暂停旧主库写入、确认副本追平、提升副本为新主库,再让应用切换连接并让旧主库跟随新主库。 跳过写入栅栏或旧主降级会留下双主写入风险。CONTROLLED SWITCHOVER · WRITE FENCE · ROLE REVERSAL应用唯一写入口Anode-1192.168.10.11:6379旧主库 · MASTER新副本 · REPLICAoffset 1280Bnode-2192.168.10.12:6379候选副本 · REPLICA新主库 · MASTERoffset 1272offset 1280 · 已追平WRITEPAUSED1280 = 1280可以提升REPLICAOF NO ONE先建立写入栅栏,阻止旧主库继续产生新数据比较复制位置;副本追平后再改变角色提升副本,但应用暂时仍不恢复写入应用改连新主库,旧主库反向跟随新主库!跳过“暂停写入”或“旧主跟随”,都可能留下双主写入窗口
写入栅栏角色切换切换后的应用与复制路径

只执行第三步会留下两个都能接受写入的节点。客户端如果同时写到两边,就会产生互相无法自动合并的数据分叉。

旧主库故障时可能只能跳过“等待追平”,因此需要接受最近写入丢失的可能。如果只是网络分区,旧主库可能仍在另一侧接受写入;无法确认它已经停止时,必须先隔离旧主库,再提升副本。旧主库恢复后也不能直接放回流量,必须先清除其旧角色并重新跟随当前主库。

🐳 双服务器最小部署

下面用两台 Linux 服务器演示复制链路:

服务器地址角色
node-1192.168.10.11主库
node-2192.168.10.12副本

配置主库与副本

示例使用 redis:8.8.0 和 host 网络。两台服务器分别准备 redis.confcompose.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_statusmaster_last_io_seconds_ago副本与主库的链路是否正常、多久没有收到复制数据
master_repl_offsetslave_repl_offset主副本偏移量差值,也就是副本大致落后多少复制流字节
repl_backlog_activerepl_backlog_size积压缓冲区是否启用、容量能否覆盖预期断线时间
sync_fullsync_partial_oksync_partial_err全量同步是否频繁、部分重同步成功率是否下降
aof_enabledaof_last_write_statusAOF 是否启用、最近一次写入是否成功

INFO replication 的部分字段仍保留 masterslave 命名,这是 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:哈希槽、客户端路由与故障转移

📚 参考资料