Skip to content

案例: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 mymaster
text
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'
done
text
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 -20
text
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 -10
text
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-milliseconds500015000容忍一次 fork/GC 停顿
failover-timeout60000120000给切换留足时间

复盘 ​

问题根因改进
部分实例 READONLY客户端写死主库 IP全部走哨兵/集群
双写数据不一致新旧主同时收到写min-replicas-to-write
误判主库下线5 秒阈值 + BGSAVE fork调到 15 秒
发现晚只监控了哨兵,没监控应用错误率加 READONLY 错误告警
bash
# 监控 READONLY 错误
grep -c 'READONLY' /var/log/order-api/app.log

切换后必做的三件事

  1. sentinel get-master-addr-by-name 确认新主。
  2. 逐个实例 info replication 确认角色。
  3. 检查应用连接池是否已刷新(很多客户端有 30 秒缓存)。

第 3 步最容易被忽略——切换后应用还在往旧主写,就是缓存没刷。