客户端在哨兵failover后仍连旧主,根本原因是未及时更新拓扑且未重建连接池;需订阅__sentinel__:hello、主动拉取权威地址、原子切换新连接池,并加超时兜底与错误重连机制。

哨兵 failover 后客户端还在连旧主节点?
根本原因是客户端没及时收到拓扑变更通知,或者收到了但没触发连接池重建。Redis 哨兵本身不主动推送完整拓扑,只通过 PUBLISH 在 __sentinel__:hello 频道广播简短消息,内容只有发送者 IP:port 和当前已知的主节点名——不含新主地址、从节点列表、甚至不带版本号或序列号,无法判断是否为最新状态。
常见错误现象:NoReplicationTargetError、ConnectionRefusedError、写请求发到已降级为从节点的老主上并报 READONLY 错误。
- 必须自己订阅
__sentinel__:hello频道,并解析每条消息中的master-name和ip:port字段 - 不能只靠一次解析就更新;需维护本地缓存的「主节点地址」和「最后收到该 master-name 消息的时间戳」,避免被延迟到达的旧消息覆盖
- 收到消息后,要立刻调用
SENTINEL get-master-addr-by-name <master-name></master-name>主动拉取当前权威地址,而不是直接信消息里的 ip:port(它可能是另一个哨兵的地址)
连接池没重建,光刷新地址也没用
很多客户端库(比如 Jedis、Lettuce 默认配置、redis-py 的 ConnectionPool)在初始化后,即使你手动改了 host/port,底层 TCP 连接、路由缓存、甚至 pipeline 状态都不会自动失效。它们不是“动态 DNS 式”刷新,而是“静态快照式”持有。
使用场景:高可用服务上线初期压测时一切正常,但真实 failover 发生后,部分实例持续失败 10–60 秒,日志里反复出现对旧地址的重试。
- 不要在运行中 patch
connection_pool.connection_kwargs—— 这不会影响已建立的连接,也不会触发新连接使用新参数 - 正确做法是:拿到新主地址后,创建全新
ConnectionPool实例,并让上层业务代码(如 DAO 层)原子切换引用;旧 pool 调用close()或交由 GC 清理 - Lettuce 用户注意:
RedisClient.setUri()不会刷新现有连接,必须重建StatefulRedisConnection;推荐用RedisClient.connect(new RedisURI())
订阅监听卡住或漏消息?检查 pub/sub 生命周期
PUB/SUB 连接天生脆弱:TCP 断开无心跳保活、重连后不自动恢复订阅、断线期间消息全丢。很多实现把监听逻辑塞进一个长期运行的线程,却没处理网络抖动或哨兵重启导致的连接中断。
性能影响:单个 pub/sub 连接阻塞会导致整个监听器停摆,后续所有拓扑变更都不可见;更糟的是,有些客户端(如早期 redis-py)在 listen() 循环里遇到 socket error 直接静默退出,不抛异常也不重连。
- 必须单独起守护线程跑
pubsub.listen(),且循环内用try/except捕获ConnectionError、TimeoutError,出错立即 close 并重建pubsub实例+重新subscribe - 不要复用业务连接做 pub/sub;专用一个短连接池(或单连接)跑监听,避免和读写流量争抢资源
- 在
message回调里只做轻量解析和触发信号(如发 event、写 atomic flag),别在里面调SENTINEL get-master-addr-by-name或重建连接池——IO 可能阻塞监听线程
为什么本地缓存拓扑比实时查哨兵更可靠?
因为 SENTINEL get-master-addr-by-name 本身有不确定性:它返回的是当前哨兵视角下的主节点,而多个哨兵之间可能因网络分区或配置漂移存在短暂不一致。如果你每次收到 hello 消息都去查一次,反而可能拿到中间态结果。
兼容性影响:Redis 6.2+ 引入了 SENTINEL replicas <master-name></master-name> 返回结构化数据,但老版本只能靠 SENTINEL slaves,字段顺序不固定,解析易错。
- 本地至少缓存两组数据:{ master-name → { ip, port, updated_at } } 和 { master-name → [ { ip, port, flags } ] }(从节点列表)
- 更新策略:仅当新查到的主地址与本地不同时,才触发连接池重建;若相同,只更新
updated_at,用于监控 staleness - 加一层超时兜底:哪怕没收到 hello 消息,也每 30 秒主动查一次哨兵,防止完全失联;但查询频率别超过 1 次/秒,避免压垮哨兵进程
真正难的不是知道该怎么做,而是得在监听线程、连接池管理、业务调用三者间划清边界——稍一混淆,就会出现「以为刷新了,其实没生效」或者「刷得太勤,把哨兵打挂了」的情况。










