主题
Redis 主从复制
一句话:主从复制把主节点的写命令异步流式发给副本,换来读扩展和故障恢复;代价是副本可能落后,主节点故障时最近写入可能丢失。
适用版本:Redis 7.x / 8.x。7.0 起 repl-diskless-sync 默认 yes。
为什么需要
单机 Redis 有两个天花板:读 QPS 受单核限制,机器挂了数据不可用。主从复制把数据复制到多个副本,读请求可以分散到副本,主节点故障时也有可提升的对象。它不解决写扩展,写仍然只有一个主节点;写扩展要靠 Cluster,见 08-集群模式。
核心机制
全量复制与增量复制
副本首次同步必然走全量;断线重连能否只补差异,取决于 offset 是否还在 backlog 范围内。
主节点维护 repl_id(复制历史标识)、master_repl_offset(发送字节数)和 replication backlog(环形缓冲区,repl-backlog-size 默认 1MB)。副本维护自己的 slave_repl_offset 和主节点的 repl_id。
以下情况会触发全量:副本首次连接;断线太久、offset 已被 backlog 覆盖;repl_id 变化(主节点重启或发生切换)。全量复制要 fork,COW 可能放大内存,生产上要尽量避免频繁全量。
无盘复制
传统全量复制是「fork → 写 RDB 到磁盘 → 读磁盘发送」;无盘复制是「fork → 直接把 RDB 流式发给副本」。7.0 起 repl-diskless-sync 默认 yes,repl-diskless-sync-delay 默认 5 秒,用于把短时间内加入的多个副本合并到同一次传输。磁盘慢、网络快时收益明显;网络抖动时可能要整份重传。
异步复制与一致性
主节点写完内存就返回客户端,不等待副本确认。所以:
- 副本可能读到旧数据,延迟取决于网络、副本负载和主节点写入量。
- 主节点故障时,尚未同步到副本的写入会丢失。
- 这是 AP 取舍:可用性和分区容错优先,不提供强一致读。
实战场景
一、backlog 只有 1MB,副本一天触发十几次全量复制
现象:一个写入量约 8 MB/s 的实例,副本每隔十几分钟就重新同步一次;监控里 master_repl_offset 与 slave_repl_offset 的差值刚被追平又拉开,主库在同步开始瞬间出现 100~300ms 的写延迟毛刺,运维最初的判断是“网络不稳定”。
排查:断线重连能否只补差异,取决于 offset 是否还在 backlog 范围内。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| backlog 大小 | CONFIG GET repl-backlog-size(默认 1MB) | 写入速率 × 断线时长远超该值 → 必然覆盖 |
| backlog 是否活跃 | INFO replication 的 repl_backlog_active、repl_backlog_first_byte_offset、repl_backlog_histlen | histlen 被冲掉即发生覆盖 |
| 是否触发全量 | 副本日志 FULLRESYNC、主库日志 Background saving started 频率 | 频繁 fork 即全量在反复发生 |
| repl_id 是否变化 | 两侧 repl_id 与 master_repl_run_id | 变化说明主节点重启或发生切换,一定走全量 |
| fork 成本 | latest_fork_usec | 大内存实例 fork 达数百毫秒到秒级 |
| 客户端缓冲 | client output buffer 报错、repl_owner 相关日志 | 网络断开的另一种表现 |
需要的 backlog ≈ 写入速率 × 最长断线恢复时间 × 冗余系数
例:8 MB/s × 60s × 2 ≈ 1 GB 级别(经验算法,需按实际写入速率校准)对症做法:
properties
# 1. 按写入速率放大 backlog,并确认它常驻(而不是被 OOM 挤掉)
repl-backlog-size 268435456 # 256MB 起步,按上面的算法核算后调整
repl-backlog-ttl 3600 # 无副本时保留 backlog 的时长
# 2. 7.0+ 无盘复制:磁盘慢、网络快时收益明显
repl-diskless-sync yes # 7.0 起默认 yes
repl-diskless-sync-delay 5 # 把短时间内加入的多个副本合并到同一次传输text
3. 主节点尽量不重启:重启会换 repl_id,所有副本只能全量。
需要打补丁时用副本先行 + 一次受控切换,而不是主节点原地重启。
4. 副本数量多且都在同一时刻重连时,用 repl-diskless-sync-delay 合并传输,
避免主库同时 fork 多次。
5. 频繁全量在容量上还要算内存:全量期间 RDB 生成 + replication buffer
会额外占用内存(下一节展开)。复盘要点:判据是“单位时间内的全量复制次数”应为 0(除计划内切换);把它从 INFO persistence 的 rdb_last_cow_size 与 INFO replication 的同步事件里统计出来。增量复制是省成本的手段,但省的前提是 backlog 按业务写入速率算过——用默认 1MB 上线等于没有这个机制。
二、一次全量复制把实例内存推到限额,进程被 OOM kill
现象:实例内存用了 5.2GB(maxmemory 6GB),新增一个副本做全量同步后,10 秒内主节点被内核 OOM killer 杀掉,哨兵随即切换。事后看 Redis 自己的 used_memory 只有 5.4GB,并未超 maxmemory,运维一时无法解释“没超限为什么会死”。
排查:maxmemory 管的是逻辑键值占用,不管 fork 之后的 COW 与复制缓冲。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 进程 RSS vs used_memory | INFO memory 的 used_memory_rss、mem_fragmentation_ratio | RSS 明显高于 used_memory 即包含 COW/缓冲 |
| fork 时的写放大 | latest_fork_usec、fork 期间的写入量 | 页被改得越多,COW 复制的页越多 |
| 复制缓冲 | 客户端与副本的 output buffer(client-output-buffer-limit replica) | 全量期间堆积在内存里 |
| RDB 生成方式 | 是否开启无盘复制 | 有盘模式还会额外占页缓存 |
| 容器/主机限额 | cgroup limit 与 dmesg 的 oom-kill 记录 | 被杀原因在进程外,不在 Redis 内 |
| AOF/持久化并发 | 是否同时在做 BGSAVE/AOF rewrite | 三个 fork 叠加时内存峰值翻倍 |
对症做法:
properties
# 1. 给 fork/COW 留物理内存:used_memory 不超过容器 limit 的 60%~70%(经验值,需按写速率校准)
maxmemory 4gb # 容器 6GB 时留出 COW + 缓冲 + 页缓存空间
# 2. 副本缓冲区限制,避免无界堆积
client-output-buffer-limit replica 512mb 128mb 60
# 3. 持久化与全量复制错峰:不要在业务高峰触发 BGSAVE / AOF rewrite
appendfsync everysec
auto-aof-rewrite-min-size 1gbtext
4. 大实例拆分片:单实例内存越大,fork 越慢、COW 峰值越高。
超过 8~10GB 的实例优先考虑用 Cluster 拆主分,而不是继续加内存。
5. 副本加入要计划执行:低峰期新增副本、一次只加一个,
多个副本同时全量的内存与网络开销会叠加。
6. 主机层面保留 swap 不是解法:Redis 落 swap 会造成秒级停顿,
正确做法是降低单实例内存规模。复盘要点:判据是“used_memory_rss / 容器 limit 长期低于阈值”,而不是只看 used_memory / maxmemory;OOM 的原因在进程物理内存,不在 Redis 的逻辑限额。全量复制要 fork、COW 可能放大内存,所以“尽量避免频繁全量”这句话包含容量含义,见 内存管理。
三、主节点故障切换后,业务“写过的数据不见了”
现象:主节点宕机,哨兵把副本提为主。业务恢复后出现两类问题:一部分刚写入的 key 完全不存在(异步复制未同步的写入丢失);另一部分是同一台应用进程连新主后读到旧数据,本地还缓存着“刚才写过”的值。运维把责任归给“Redis 不可靠”。
排查:这是 AP 取舍的既定结果,不是故障。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 写入是否已同步 | 宕机前主库 master_repl_offset 与该副本 slave_repl_offset 的差 | 差值区间内的写入就是丢失部分 |
| 复制模式 | 是否只用异步复制(默认) | 主写完内存即返回,不等副本 |
| 切换后的 repl_id | 新主的 repl_id 与旧主不同 | 所有副本走全量,期间数据不可用 |
| 客户端行为 | 应用是否重连、是否清掉本地缓存 | 旧值残留是第二类症状的来源 |
| 业务能否接受 | 该数据是“可重建缓存”还是“唯一真相” | 决定要不要把 Redis 当存储 |
对症做法:
java
// 1. 关键写用 WAIT 要求 N 个副本确认(仍不是强一致,只是缩小丢失窗口)
Long acked = jedis.waitForReplica(1, 100); // 等待 1 个副本、最多 100ms
if (acked == null || acked < 1) metrics.replicaLagRisk(); // 记指标,不阻断业务text
2. 区分用途:
- 缓存类数据:允许丢,主库故障后由数据库重建,业务无需感知;
- 唯一真相类数据(计数器、库存、分布式锁状态):不能只放 Redis,
必须落 DB,Redis 只做加速。
3. 切换后主动失效本地缓存:订阅键空间通知或在客户端监听连接重置事件,
清掉 Caffeine 里的“刚写入”值,避免读到已丢失数据的旧副本。
4. 幂等设计覆盖“写成功后丢失”的场景:
业务重试时以数据库当前状态判定,不假设 Redis 里的值一定存在。
5. 强一致需求要换方案(读写同一节点、或用 DB 事务、或 Redlock 类协议的局限),
不要用异步复制的 Redis 承担强一致。复盘要点:判据是把每个 Redis key 标成“缓存/真相”两类,第二类必须能在故障演练中证明“丢了也能从 DB 重建”。演练口径:kill 主节点,断言未同步写入的丢失集合与恢复路径一致。副本可读是它的价值,但读到的可能是旧数据,这一点在读策略上必须显式处理,见 集群模式。
最小上手
bash
# 主节点
redis-server --port 6379
# 副本(另开终端)
redis-server --port 6380 --replicaof 127.0.0.1 6379
# 确认复制状态:role:slave、master_link_status:up、lag 接近 0
redis-cli -p 6380 INFO replication
# 在主节点上让写入等待至少 1 个副本确认(Redis 3.0+)
redis-cli -p 6379 WAIT 1 100
# 手动把副本提升为主(演练/应急)
redis-cli -p 6380 REPLICAOF NO ONE生产动作
- backlog 按写入速率估:
repl-backlog-size默认1MB,估算为「主节点每秒写入字节 × 最长允许断线秒数 × 2」。太小会导致稍一断线就全量复制。 - 写安全取舍:
min-replicas-to-write(默认0,关闭)和min-replicas-max-lag(默认10秒)配合使用。例如设为1和10,当没有 lag ≤10 秒的副本时主节点拒绝写入。它降低丢数据概率,但副本故障时会把主节点变成不可写,是可用性换一致性。 WAIT不是持久化保证:WAIT numreplicas timeout(3.0+)只等到副本在内存中确认收到,不保证副本已 fsync;副本随后崩溃仍可能丢。- 延迟排查:
INFO replication看lag、master_repl_offset与slave_repl_offset的差值;repl-disable-tcp-nodelay no(默认)保持低延迟。 - 读副本要接受旧数据:强一致读走主节点,或业务层做版本/时间戳校验。
- 避免频繁全量:关注
sync_full、sync_partial_ok、sync_partial_err,全量次数异常升高通常是 backlog 太小或网络不稳。
边界
- 异步复制不提供强一致:主节点返回成功不代表副本已有数据,切换时可能丢最近写入。
min-replicas-*不是银弹:它只能降低风险,不能保证副本已落盘;配置不当还会让主节点在副本故障时拒绝所有写入。WAIT只保证「收到」:确认发生在副本内存层,副本进程或机器崩溃后数据仍可能丢。- 副本不是备份:误删会同步到所有副本;备份要独立落盘。
- 主从不是分片:所有节点存全量数据,容量仍受单机内存限制。
讲清楚
主从复制的本质是一条异步的命令流:主节点把写命令发给副本,副本按 offset 回放。首次同步或断线太久时走全量,靠 fork 生成 RDB;平时走增量,靠 backlog 补差异。它带来读扩展和故障恢复能力,但不带来强一致——主节点写完就返回,副本可能落后,主节点挂了最近的写入可能丢。
想降低丢数据概率,可以用
WAIT等副本确认,或用min-replicas-to-write在副本跟不上时拒绝写入;但两者都不是持久化保证,真正的强一致要换 CP 系统。