根本原因是连接池未主动刷新拓扑且未校验节点角色,导致持续复用连向旧主(现为从)的established连接;需强制dns刷新、监听+switch-master事件、每次写前执行role命令校验角色,并禁用statefulredisconnection单例复用。

连接池还在用旧主地址,因为没触发销毁重建
根本原因不是“客户端没收到通知”,而是 JedisSentinelPool 或 Lettuce 没真正执行拓扑刷新动作,导致连接池里全是连向已下线旧主的连接。这些连接在 TCP 层可能还处于 ESTABLISHED 状态(尤其在 K8s Service 场景下),PING 也成功,但一发写命令就报 READONLY You can't write against a read only replica。
常见触发失败点:
-
JedisSentinelPool初始化时只传了一个哨兵地址,单点失效后无法拉取+switch-master事件 - 应用日志里搜不到
Discovered new master,说明没监听到哨兵的__sentinel__:hello频道,或哨兵之间通信异常 - 使用了
JedisPool而非JedisSentinelPool,完全绕过了 Sentinel 自动发现逻辑 -
Lettuce启用了RedisClient.connect(SENTINEL_URI),但ClientOptions中显式关闭了discovery,或未配置ClusterTopologyRefreshOptions
DNS 缓存让连接池永远连错 IP
即使哨兵返回了新主的域名(比如 redis-master.default.svc.cluster.local),Java 默认会把 DNS 解析结果永久缓存(sun.net.inetaddr.ttl = -1),导致连接池反复尝试连一个已不存在的旧 IP。
必须主动打破这层缓存:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 启动 JVM 时加参数:
-Dsun.net.inetaddr.ttl=30(30 秒刷新一次) - 或在代码中运行一次:
InetAddress.setCachePolicy(30),比 JVM 参数更灵活 - K8s 环境下若用 headless Service,还要确认 endpoints 是否已更新 ——
kubectl get endpoints redis-master,看 IPs 是否已切换
testOnBorrow 不等于角色校验,它拦不住 READONLY 错误
testOnBorrow=true 只是发个 PING,只要节点活着、端口通,就认为连接可用。但旧主切完后变成从节点,PING 成功,SET 却必然失败。这是最隐蔽的坑。
真正要做的不是“检测连接通不通”,而是“检测节点是不是主”:
- 每次写操作前,先执行
ROLE命令,检查返回值第一个元素是否为"master" - 若为
["slave", "192.168.1.10", "6379"],立刻丢弃当前连接,重新从连接池获取或重建连接池 - 不要依赖
INFO replication,它输出格式不稳定;ROLE是 Redis 官方保证结构一致的命令
连接复用导致 StatefulRedisConnection 持有已失效引用
Lettuce 的 StatefulRedisConnection 是长生命周期对象。一旦底层 socket 断开或节点角色变更,这个实例不会自动失效,应用层如果长期持有它(比如注入为 Spring Bean),后续所有命令都会打到旧地址。
不能靠“等它断”来解决问题:
- 避免将
StatefulRedisConnection声明为单例 Bean;应每次需要时调用RedisClient.connect()获取新实例 - 若必须复用,每次写操作前必须做可用性探测:
connection.sync().ping(),捕获RedisCommandExecutionException后立即close()并重建 - 禁用连接复用的最简单方式:把
RedisClient配置里的timeout设为2000,maxAttempts设为3,让失败快速暴露
PING 得通却写不了的连接——它们安静地躺在池子里,直到业务高峰突然集体爆发错误。










