一场凌晨的故障:从告警到冷汗
凌晨3点,我正盯着手机上的告警:「Redis集群主节点故障,触发自动切换」。这本该是个好消息——Sentinel正常工作了,但接下来的监控曲线却让我冷汗直冒:客户端QPS断崖式下跌,部分请求超时,而旧主节点的写入流量居然还在持续!我们用的是Java生态,客户端是Jedis,Redis版本5.0。集群规模不大(6节点,单主双从),但承载了核心业务的缓存和分布式锁。问题来了:为什么主从切换后,有些客户端像“瞎了”一样,依然固执地向已经降级的旧主写入数据?
根因:客户端“固执”的背后
1. Jedis的拓扑缓存陷阱
Jedis默认会缓存集群拓扑信息(包括节点角色)。当Sentinel完成主从切换后,Jedis不会立即感知到拓扑变化,除非:
- 显式调用
JedisCluster#clusterSlots或JedisCluster#refreshCluster - 当前连接触发了
MOVED或ASK重定向错误 - 关键机制:Jedis的拓扑刷新是惰性 的。如果你没有配置定时刷新或触发重定向,客户端可能直到连接超时才会发现主节点变化。
2. 连接池的“雪上加霜”
更糟糕的是,连接池中的长连接会复用旧主节点的TCP连接。即使Sentinel广播了新主节点信息,这些“僵尸连接”依然会继续向旧主写入——直到旧主彻底拒绝连接或超时。
// 错误写法:没有处理拓扑刷新的JedisCluster初始化 JedisCluster jedis = new JedisCluster(nodes, timeout, poolConfig); // 请求仍然可能被路由到旧主 jedis.set("key", "value"); // 正确写法:配置自动刷新(需Jedis 3.7+) ClusterClientOptions options = ClusterClientOptions.builder() .autoReconnect(true) .pingBeforeActivateConnection(true) .topologyRefreshOptions( TopologyRefreshOptions.builder() .enableAllAdaptiveRefreshTriggers() // 自适应触发刷新 .refreshTriggersReconnectAttempts(3) .build() ).build(); JedisCluster jedis = new JedisCluster(nodes, timeout, timeout, 5, password, poolConfig);数据对比:刷新策略的影响
我们在测试环境模拟主从切换,对比不同配置下的恢复时间:
| 配置方案 | 平均感知延迟 | 数据丢失风险 |
|---|---|---|
| 默认无刷新 | 5-30秒 | 高 |
| 定时刷新(30秒) | <5秒 | 中 |
| 自适应刷新+重试 | <1秒 | 低 |
- 结论:仅靠定时刷新不够——网络分区或Sentinel延迟可能导致刷新失效,自适应刷新(基于错误触发)更可靠。
避坑清单:你必须知道的4个细节
JedisCluster#close并重建实例是最彻底的方式)。终极解法:从客户端到架构的防御
最终我们的解决方案是
分层防御:PING)检测连接健康度INFO replication实时比对客户端与服务端的主节点视图// 监控示例:定期检查主节点一致性 public void checkMasterConsistency(JedisCluster jedis) { String currentMaster = jedis.clusterNodes().split("\n") .stream().filter(line -> line.contains("myself,master")) .findFirst() .orElseThrow(); if (!currentMaster.equals(lastKnownMaster)) { logger.warn("Master changed from {} to {}", lastKnownMaster, currentMaster); jedis.close(); // 强制重建连接 } }写在最后
Redis的主从切换不是“银弹”,客户端的拓扑感知比你想象的更脆弱。
真正的稳定性来自于“不信任任何自动故障转移”的防御式编程。你在项目中是怎么处理这类问题的?欢迎评论区聊聊你的“血泪史”。