第 17 章
复制延迟、一致性与数据丢失场景
第17章 复制延迟、一致性与数据丢失场景
Redis 主从复制默认是异步的。这意味着主库确认写入、但从库尚未收到数据的窗口期是客观存在的。本章通过四个典型场景模拟数据丢失,分析 min-replicas-to-write、WAIT 命令、以及复制积压溢出引发的恶性循环,帮助你在生产中精确把握可用性与数据安全的权衡点。
17.1 异步复制的本质
主库收到 SET k v 后的处理顺序:
1. 写入内存(AOF 异步持久化)
2. 返回 +OK 给客户端 ← 客户端认为写入成功
3. 将命令写入 repl_backlog
4. 异步推送给从库(通过 output buffer)
5. 从库执行命令,更新自身 offset
步骤2和步骤5之间存在时间差,这就是复制延迟(replication lag)。正常情况下延迟 <10ms,但在网络抖动、主库高负载或从库处理能力不足时,延迟可达数秒乃至更长。
17.2 数据丢失场景模拟
场景1:主库崩溃 + 从库延迟1秒
时间线:
T=0: 客户端向主库写入 10000 条命令(~50MB)
T=0.5: 主库已写入 10000 条,返回 OK
T=0.5: 从库已同步 7000 条(延迟500ms)
T=1.0: 主库服务器宕机(断电/OOM Kill)
T=1.1: Sentinel 检测到主库下线
T=30s: Sentinel 完成故障转移,从库提升为主库
结果: 3000 条命令(T=0.5s时从库落后的那部分)永久丢失
丢失数据量计算:
write_rate = 100_000 # 条/秒
lag_seconds = 1.0 # 从库延迟
lost_commands = write_rate * lag_seconds # 10万条
缓解措施:
# 开启 AOF 并设置 fsync=always(性能损失约30%,但不丢数据)
appendonly yes
appendfsync always
# 或者使用 WAIT 命令(见17.4节)
场景2:网络分区 + 脑裂(Split-Brain)
这是最危险的数据丢失场景:
初始状态:
[主库 M] ←→ [从库 S1] ←→ [从库 S2]
[Sentinel1][Sentinel2][Sentinel3]
T=0: 网络分区,M 与 S1/S2/Sentinel 断开
客户端 Client-A 仍能访问 M(M 在单独分区内)
T=0~30s: Client-A 持续向 M 写入(M 不知道自己已经是"孤岛")
T=30s: Sentinel 判定 M 客观下线,选举 S1 为新主
Client-A 被通知新主地址,开始向 S1 写入
T=60s: 网络分区恢复,旧 M 重新上线
旧 M 收到 SLAVEOF S1 的指令,变为从库
旧 M 与 S1 进行全量同步,丢弃自己在分区期间接收的所有写入
丢失量 = 分区持续时间 × 写入速率
防护配置:
# min-replicas-to-write:至少有 N 个从库确认,主库才接受写入
min-replicas-to-write 1
# min-replicas-max-lag:从库延迟不超过 M 秒才算有效
min-replicas-max-lag 10
开启上述配置后,网络分区发生时:
- 主库发现自己无法与任何从库通信
- 10秒后(
min-replicas-max-lag),主库拒绝新的写入请求(返回错误) - 客户端报错而非丢数据
代价:可用性降低(主库无从库时拒绝写入)。
场景3:BGSAVE 期间 COW 导致 output buffer 溢出
T=0: 主库开始 BGSAVE(fork 子进程)
T=0~5s: 大量写入,触发 COW(Copy-on-Write)
主库内存从 10GB 膨胀到 16GB
每秒新写入 100MB,从库 output buffer 以 100MB/s 速率增长
T=4s: 从库 output buffer 达到 256MB(hard limit)
T=4s: 主库强制断开从库连接
T=4s: 从库发现连接断开,尝试重连
T=5s: 从库 PSYNC,由于断连期间 backlog 已被覆盖,触发全量同步
T=5s: 主库再次 BGSAVE(还在进行!叠加)
→ 恶性循环
完整解决方案:
# 1. 调大 output buffer
client-output-buffer-limit replica 1gb 256mb 300
# 2. 使用无盘复制,减少 BGSAVE 期间的写压力
repl-diskless-sync yes
# 3. 增大 backlog(断连后可部分重同步,避免再次 BGSAVE)
repl-backlog-size 2gb
# 4. 限制 BGSAVE 触发频率
save "" # 禁用自动 RDB(如果使用 AOF)
# 5. 监控 output buffer
watch -n1 "redis-cli client list | grep replica | awk '{print \$15}'"
场景4:min-replicas-to-write 反向影响可用性
配置:min-replicas-to-write 1,min-replicas-max-lag 10
场景:唯一从库发生延迟(慢查询/网络抖动),lag > 10s
结果:
- 主库拒绝所有写入命令:
(error) NOREPLICAS Not enough good replicas to write.
- 数据并未丢失,但业务完全不可用
更精细的权衡策略:
# 业务分层:
# - 关键数据(余额、订单):WAIT 等待确认
# - 普通数据(日志、统计):异步写入,允许少量丢失
# 关键写入示例:
redis.set("account:balance:1001", 5000)
ack = redis.wait(1, 1000) # 等待1个从库确认,超时1秒
if ack < 1:
# 记录警告日志,可能需要补偿
logger.warning("Replication not confirmed")
17.3 min-replicas-to-write 与 min-replicas-max-lag
参数语义
# redis.conf
min-replicas-to-write 1 # 至少1个从库确认才接受写入
min-replicas-max-lag 10 # 从库延迟不超过10秒才算"有效从库"
主库每秒检查一次从库的 lag 值(通过 ACK 心跳计算)。
判断逻辑
/* replication.c */
int replicationCountAOFAcksByOffset(long long offset) {
/* 统计 lag <= min-replicas-max-lag 的从库数量 */
int count = 0;
listIter li;
listNode *ln;
listRewind(server.slaves, &li);
while((ln = listNext(&li))) {
client *slave = ln->value;
if (slave->repl_ack_time < server.unixtime - server.repl_min_slaves_max_lag)
continue; /* lag 超限,不计入 */
count++;
}
return count;
}
实际效果验证
# 模拟从库延迟(从库侧)
redis-cli debug sleep 20
# 观察主库(20秒内写入是否被拒绝)
redis-cli set test_key test_value
# (error) NOREPLICAS Not enough good replicas to write.
# 从库恢复后,写入恢复正常
三种配置模式对比
| 模式 | 配置 | 数据安全 | 可用性 | 适用场景 |
|---|---|---|---|---|
| 纯异步 | min-replicas-to-write 0 | 低 | 高 | 缓存、可丢失数据 |
| 半同步 | min-replicas-to-write 1,lag 10 | 中 | 中 | 一般业务数据 |
| WAIT强确认 | WAIT 2 500 | 高 | 低(有超时) | 金融、订单数据 |
17.4 WAIT 命令:同步等待特定数量从库确认
命令语法
WAIT numreplicas timeout_ms
→ 返回:已确认该命令(及之前所有写入)的从库数量
注意:WAIT 的语义是等待已知偏移量被指定数量的从库确认,而不是等待某个具体命令被确认。
工作原理
1. WAIT 发出时,主库记录当前 master_repl_offset(即 X)
2. 主库向所有从库发送 REPLCONF GETACK *(要求从库立即上报 offset)
3. 主库等待,直到有 numreplicas 个从库上报 offset >= X
4. 超时或满足条件后返回
使用示例
import redis
r = redis.Redis(host='master_ip', port=6379)
# 关键写入
pipe = r.pipeline()
pipe.set("account:balance:1001", 5000)
pipe.set("order:status:9999", "paid")
pipe.execute()
# 等待至少1个从库确认,最多等待500ms
ack_count = r.wait(1, 500)
if ack_count >= 1:
print(f"Write confirmed by {ack_count} replica(s)")
else:
print("WARNING: Replication not confirmed within timeout")
# 根据业务需要决定是否回滚或告警
WAIT 返回值含义
WAIT 1 0 → 返回 0:没有从库确认(timeout=0表示立即返回当前状态)
WAIT 1 500 → 返回 1:在500ms内1个从库确认
WAIT 2 500 → 返回 1:只有1个从库确认(不足2个,但不报错)
返回值 小于 numreplicas 不是错误,只是告知实际确认数量。业务层需要自行判断是否满足要求。
WAIT 的性能影响
# 基准测试:WAIT vs 不等待
redis-benchmark -n 10000 -c 50 -P 16 SET key value
# 吞吐量:约 200,000 ops/sec
redis-benchmark -n 10000 -c 1 --wait 1 500 SET key value
# 吞吐量:约 5,000 ops/sec(串行等待,性能大幅下降)
# 建议:仅对关键写入使用 WAIT,批量使用 Pipeline+WAIT
17.5 复制延迟监控
INFO replication 详解
redis-cli info replication
输出示例:
# Replication
role:master
connected_slaves:2
slave0:ip=192.168.1.2,port=6380,state=online,offset=1234560,lag=0
slave1:ip=192.168.1.3,port=6381,state=online,offset=1234100,lag=1
master_replid:8371b4fb1155b71f4a04d3e1bc3e18c4a990aeeb
master_repl_offset:1234567
关键字段说明:
lag=0:延迟 <1 秒(因为 lag 以整数秒为单位)offset:从库最后一次 ACK 的偏移量master_repl_offset - slave_offset:字节级延迟
精确延迟计算
master_offset=$(redis-cli -h master info replication | grep master_repl_offset | cut -d: -f2 | tr -d '\r')
slave_offset=$(redis-cli -h slave info replication | grep master_repl_offset | cut -d: -f2 | tr -d '\r')
lag_bytes=$((master_offset - slave_offset))
echo "Replication lag: ${lag_bytes} bytes"
Prometheus 监控指标
# redis_exporter 自动暴露以下指标:
redis_replication_lag{addr="slave1:6380"} 0 # 秒级延迟
redis_connected_slaves 2 # 从库数量
redis_repl_backlog_histlen 536870912 # backlog 使用量(bytes)
# 推荐告警规则:
- alert: RedisReplicationLag
expr: redis_replication_lag > 30
for: 1m
labels:
severity: warning
annotations:
summary: "Redis replication lag > 30s on {{ $labels.addr }}"
- alert: RedisReplicationBacklogFull
expr: redis_repl_backlog_histlen / redis_repl_backlog_size > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "Redis replication backlog nearly full (>90%)"
实时监控脚本
#!/bin/bash
# monitor_replication.sh
MASTER="192.168.1.1:6379"
SLAVES=("192.168.1.2:6380" "192.168.1.3:6381")
while true; do
master_offset=$(redis-cli -h ${MASTER/:*/} -p ${MASTER/*:/} info replication \
| grep master_repl_offset | cut -d: -f2 | tr -d '\r')
echo "=== $(date) ==="
echo "Master offset: $master_offset"
for slave in "${SLAVES[@]}"; do
host=${slave/:*/}
port=${slave/*:/}
slave_offset=$(redis-cli -h $host -p $port info replication \
| grep master_repl_offset | cut -d: -f2 | tr -d '\r')
lag_bytes=$((master_offset - slave_offset))
echo "Slave $slave: offset=$slave_offset, lag=${lag_bytes} bytes"
done
sleep 1
done
17.6 复制积压溢出触发全量重同步的解决路径
问题诊断
# 从库日志(出现以下内容表示触发全量同步)
redis-cli monitor 2>/dev/null | grep -i "fullresync\|sync\|slave"
# 主库日志关键词:
# "Starting BGSAVE for SYNC with target: disk"
# "Starting BGSAVE for SYNC with target: slaves sockets"
# "Synchronization with slave 192.168.1.2:6380 succeeded"
防止积压溢出的配置矩阵
| 写入速率 | 可接受断连时间 | 建议 backlog 大小 |
|---|---|---|
| <10 MB/s | 30s | 1GB |
| 10-50 MB/s | 30s | 3GB |
| 50-100 MB/s | 60s | 12GB |
| >100 MB/s | 考虑 Cluster 分片 | N/A |
紧急处置流程
当发现从库频繁全量同步时:
# Step 1: 临时增大 backlog(不重启)
redis-cli -h master config set repl-backlog-size 2147483648 # 2GB
# Step 2: 临时增大 output buffer(不重启)
redis-cli -h master config set client-output-buffer-limit \
"replica 1gb 256mb 300"
# Step 3: 确认从库已稳定(lag 趋近于0)
watch -n2 "redis-cli -h master info replication | grep slave"
# Step 4: 将配置写入 redis.conf(持久化)
redis-cli -h master config rewrite
17.7 主从一致性模型总结
Redis 主从复制提供的是最终一致性(Eventual Consistency):
正常情况:延迟 <10ms,几乎是强一致
抖动情况:延迟 100ms~1s,短暂不一致
极端情况:断连数秒,数据可能永久丢失
CAP 定理定位:
- 主从模式(哨兵):CP 倾向(故障转移期间拒绝写入)或 AP 倾向(取决于配置)
- min-replicas-to-write=0:AP(优先可用性)
- min-replicas-to-write=1:CP(优先一致性,牺牲部分可用性)
从库读取一致性保障
# 问题:写主库后立刻读从库,可能读到旧值
redis_master.set("user:1:name", "Alice")
# 此时从库可能还是旧值
name = redis_slave.get("user:1:name") # 可能是 None 或旧值
# 方案1:写后读走主库
name = redis_master.get("user:1:name") # 强一致
# 方案2:写后等待(WAIT)再读从库
redis_master.set("user:1:name", "Alice")
redis_master.wait(1, 100)
name = redis_slave.get("user:1:name") # 99%概率已更新
# 方案3:读写分离时接受短暂不一致(适合缓存场景)
本章小结
| 场景 | 根因 | 防护手段 |
|---|---|---|
| 主库宕机丢最近写入 | 异步复制延迟 | AOF fsync=always 或 WAIT |
| 网络分区脑裂 | 孤岛主库持续接受写入 | min-replicas-to-write |
| output buffer 溢出恶性循环 | BGSAVE COW + 积压 | 调大 buffer + diskless + 大 backlog |
| min-replicas 配置拒写 | 从库延迟超阈值 | 监控 lag,优化从库性能 |
Redis 异步复制的数据安全与可用性是一个持续权衡的过程。第18章将进入哨兵模式,深入分析 failover 状态机与 Raft 选主协议。