主节点切换必然导致长连接断开,因旧主进程下线后操作系统回收socket,所有指向该ip:port的连接立即失效;客户端须主动重建连接,否则命令将卡住或抛出connectionreseterror等错误。

主节点切换必然导致长连接断开,这不是配置问题,而是 TCP 连接绑定到具体 IP:PORT 的物理事实——旧主进程下线后,操作系统回收其 socket 句柄,所有指向该地址的连接立即失效。客户端若不主动重建,后续命令会卡住或抛出 ConnectionResetError、ConnectionRefusedError、READONLY You can't write against a read only replica。
为什么 pingBeforeActivateConnection 不能替代连接池健康检查
很多用户以为开启 pingBeforeActivateConnection=true(Lettuce)或 testOnBorrow=true(Jedis)就万事大吉,其实不然:
- 它只在从连接池取连接时触发一次 PING,但连接可能在归还后、下次取出前就已断开(比如中间设备静默 kill 了空闲连接)
- Jedis 的
testOnBorrow默认关闭,且开启后每次取连接都发 PING,高并发下反而增加延迟和集群负载 - Lettuce 的
pingBeforeActivateConnection对已建立的StatefulRedisConnection实例无效——应用层若长期持有该实例,它不会自动刷新底层 socket - 真正起作用的是周期性健康检查:Lettuce 需配
ClientOptions.builder().socketOptions(SocketOptions.builder().keepAlive(true))+ 后台心跳;redis-py 要设health_check_interval=30
写命令绝对禁止自动重试,读命令重试必须带 slot 刷新
集群模式下,重试不是换个节点再发一次那么简单。主从切换后,slot 映射可能已变更,客户端缓存的拓扑过期会导致命令路由到错误节点,甚至写入从库报错。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 写命令(
SET、HSET、INCR)必须失败即止,不可重试——否则可能因幂等缺失造成数据覆盖或计数错乱 - 读命令(
GET、HGET、EXISTS)可有限重试,但每次重试前必须强制刷新 slot 缓存:client.connection_pool.reset()(redis-py)、connection.getPartitions().reload()(Lettuce) - 不要依赖 SDK 默认的
retry_on_timeout=True:它对连接已断无效,且集群下无法区分“超时”和“重定向失败”,容易把MOVED错当故障重试 - 手动封装读操作时,退避时间建议用
time.sleep(0.1 * (2 ** i)),最多 2 次,避免雪崩式重试压垮新主
JedisSentinelPool 和 Lettuce 在切换后的真实行为差异
两者都会响应 +switch-master 事件,但触发时机和清理粒度不同,直接影响你是否看到“连着旧主”的现象:
-
JedisSentinelPool收到事件后调用destroy()清空整个连接池,下次getResource()必然新建连接——前提是它真收到了事件;需确认日志有Discovered new master,否则检查哨兵列表是否完整、订阅频道__sentinel__:hello是否被防火墙拦截 -
Lettuce使用RedisClient.connect(SENTINEL_URI)时,默认后台定时刷新拓扑(间隔 30 秒),但不会销毁已有连接;如果你在应用层缓存了StatefulRedisConnection实例,它仍会复用旧 socket,必须显式调用connection.sync().ping()验证,失败则RedisClient.connect()重建 - 共性陷阱:DNS 缓存。Java 默认永久缓存 DNS 结果(
sun.net.inetaddr.ttl=-1),即使哨兵返回新主 IP,JVM 仍解析为旧地址;务必设-Dsun.net.inetaddr.ttl=30
最易被忽略的一点:重连成功不等于命令可用。你得验证新连接是否真的连上了当前主节点,而不是某个尚未同步完成的从库——最简单的办法是在每次写操作前,用 client.info("replication") 检查 role:master 字段,或直接 client.config_get("slaveof") 确认为空。










