外观
案例:Redis 主从切换后写入旧主
现象
监控显示订单写入失败率突增,应用日志:
text
org.springframework.data.redis.RedisSystemException:
READONLY You can't write against a read only replica.同时另一部分实例写入成功,但事后查数据发现两部分数据不一致。
定位过程
第一步:确认当前真正的主
bash
redis-cli -p 26379 sentinel get-master-addr-by-name mymastertext
1) "10.0.0.12"
2) "6379"哨兵认为 10.0.0.12 是主。再看三台真实角色:
bash
for h in 10.0.0.11 10.0.0.12 10.0.0.13; do
echo -n "$h: "
redis-cli -h $h -a "$PASS" info replication | awk -F: '/^role/{print $2}' | tr -d '\r'
donetext
10.0.0.11: slave
10.0.0.12: master
10.0.0.13: slave集群本身是正确的。问题在客户端。
第二步:看应用连的是谁
bash
redis-cli -h 10.0.0.11 -a "$PASS" client list | awk '{print $2, $6, $10}' | head其实更直接的证据在应用配置:
yaml
spring:
data:
redis:
host: 10.0.0.11 # 写死的 IP
port: 6379一部分实例配置的是 10.0.0.11(旧主),切换后它变成从库 → READONLY。另一部分配置的是 10.0.0.12 → 写入成功。
双写不一致的根因就在这里。
第三步:为什么切换了
bash
grep -E 'sdown|odown|switch-master|convert-to-slave' /var/log/redis/sentinel.log | tail -20text
03:12:04 +sdown master mymaster 10.0.0.11 6379
03:12:04 +odown master mymaster 10.0.0.11 6379 #quorum 2/2
03:12:04 +try-failover master mymaster 10.0.0.11 6379
03:12:04 +elected-leader master mymaster 10.0.0.11 6379
03:12:04 +failover-state-select-slave master mymaster 10.0.0.11 6379
03:12:04 +selected-slave slave 10.0.0.12:6379 10.0.0.12 6379 @ mymaster 10.0.0.11 6379
03:12:04 +promoted-slave slave 10.0.0.12:6379 10.0.0.12 6379 @ mymaster 10.0.0.11 6379
03:12:04 +failover-state-reconf-slaves master mymaster 10.0.0.11 6379
03:12:04 +switch-master mymaster 10.0.0.11 6379 10.0.0.12 6379+sdown 和 +odown 发生在同一秒,down-after-milliseconds 只有 5000ms。再查旧主日志:
bash
grep -Ei 'latency|fork|slow' /var/log/redis/6379.log | tail -10text
03:11:58 * Background saving started by pid 18241
03:12:03 * Background saving terminated with success
03:12:05 # Client id=1842 addr=10.0.0.12:51244 closed (replica)BGSAVE 期间主线程被 fork 阻塞,加上 5 秒的判定阈值过于激进,导致误判。
处置
立即
bash
# 1. 应用配置改成哨兵模式(或集群),不要写死 IP
# 2. 滚动重启应用实例yaml
spring:
data:
redis:
password: ${REDIS_PASS}
sentinel:
master: mymaster
nodes: 10.0.0.11:26379,10.0.0.12:26379,10.0.0.13:26379数据对账
bash
# 找出旧主上的"脏数据":key 的 TTL/内容与新主不一致
redis-cli -h 10.0.0.11 -a "$PASS" --scan --pattern 'order:*' | head -20旧主已经被降级,全量同步后它的数据会以新主为准。故障期间写入旧主的数据会丢,只能靠业务侧对账补。
防脑裂
ini
# 主库配置
min-replicas-to-write 1
min-replicas-max-lag 10网络分区后,旧主发现"健康从库不足 1 个",直接拒绝写入(返回 NOREPLICAS),从根上避免双写。
调优哨兵
ini
sentinel down-after-milliseconds mymaster 15000
sentinel failover-timeout mymaster 120000| 参数 | 原值 | 新值 | 理由 |
|---|---|---|---|
down-after-milliseconds | 5000 | 15000 | 容忍一次 fork/GC 停顿 |
failover-timeout | 60000 | 120000 | 给切换留足时间 |
复盘
| 问题 | 根因 | 改进 |
|---|---|---|
| 部分实例 READONLY | 客户端写死主库 IP | 全部走哨兵/集群 |
| 双写数据不一致 | 新旧主同时收到写 | min-replicas-to-write |
| 误判主库下线 | 5 秒阈值 + BGSAVE fork | 调到 15 秒 |
| 发现晚 | 只监控了哨兵,没监控应用错误率 | 加 READONLY 错误告警 |
bash
# 监控 READONLY 错误
grep -c 'READONLY' /var/log/order-api/app.log切换后必做的三件事
sentinel get-master-addr-by-name确认新主。- 逐个实例
info replication确认角色。 - 检查应用连接池是否已刷新(很多客户端有 30 秒缓存)。
第 3 步最容易被忽略——切换后应用还在往旧主写,就是缓存没刷。