Redis 连接池里的"僵尸连接":服务端空闲回收后客户端不报错,如何系统性根治?
该问题的正文与回答。
【背景】
我们用 Redis 6.2(3 主 3 从 + Sentinel)承载会话与限流计数,Java 8 + Spring Boot 2.7,客户端是 Jedis 3.8(自封装,基于 JedisPool,maxTotal=200、maxIdle=50、minIdle=10),应用侧 QPS 峰值约 3k,平均 RT 1ms 左右。生产 Redis 配了 timeout 300(空闲 5 分钟服务端主动断开),网络是阿里云内网 VPC。
【现象】
凌晨低峰跑了一夜之后,白天业务上来时偶发 JedisConnectionException: java.net.SocketException: Connection reset by peer,频率大约每天 20~50 次,集中在业务从低峰拉起后的前 1~2 分钟。我猜的是:minIdle=10 的常驻连接在服务端被 timeout 300 回收了,但客户端 pool 里那 10 个连接对象还在,业务第一个请求抽到"僵尸连接"就炸。用打比方说,就像餐厅里桌子被服务员收走了,客人以为还在,一坐下才发现没有。
【已尝试的排查与结果】
- 开
testOnBorrow=true+testWhileIdle=true(minEvictableIdleTimeMillis=60s,timeBetweenEvictionRunsMillis=30s),错误明显减少但没归零,怀疑 evict 线程跑得不够快,业务线程还是抢到了死连接; - 把
timeout 300调大到 3600,错误基本消失,但这是"绕"不是"治",而且担心长连接被中间设备(LB / NAT)静默断掉后问题换个姿势再来; - 翻过 Jedis 源码,发现 JedisPool 默认没有类似 Lettuce 的 ping 自动重连机制,也没有
testOnReturn的合理默认值。
【期望得到的答案形态】 希望有经验的同学给出:
- 一套可落地的参数组合(JedisPool 的 testOnBorrow / testWhileIdle / minEvictableIdleTimeMillis / timeBetweenEvictionRunsMillis,配合服务端 timeout),并解释每个参数为什么这么配、边界在哪里;
- 如果切 Lettuce,
validatePipelinedResponses、ping间隔、shutdownGracefully这些和 Jedis 的对应关系; - 有没有更"治本"的方案,比如客户端主动发 PING 保活、或者用 Redis 的
CLIENT LIST对账? 最好能贴你线上跑过的配置 + 数据(错误率从多少降到多少),有具体数字我会采纳。
【更新 2026-10-23:补充三组对比实验数据】 后来我按社区几位同学的建议做了三组对比实验,贴一下数据:
-
Jedis 3.8 + testOnBorrow=true + testWhileIdle=true(minEvictableIdleTimeMillis=60s,timeBetweenEvictionRunsMillis=30s)+ 服务端 timeout=300:
- 错误率从 0.003%(20~50 次/天)降到 0.0004%(3~5 次/天)
- 残留错误集中在 evict 线程刚跑完、业务线程立刻 borrow 的窗口(约 50ms),Jedis 的 evict 是单线程异步的,这个窗口无法完全消除
-
切 Lettuce 5.1.8(shared connection + validateCommands=true + ping 间隔 60s):
- 错误率归零(连续 7 天 0 次 Connection reset)
- 代价:首次 borrow 多 ~0.2ms(PING 往返),QPS 3k 下可忽略
- 关键配置:LettuceClientConfiguration.builder().validateEndpoint(true).pingBeforeActivateConnection(true)
- 注意:Sentinel 模式下主从切换后 shared connection 全部失效,Lettuce 会自动重连但需要 200~500ms 窗口,这个窗口内请求会失败
-
最"治本"的方案:服务端 timeout 改为 1800(> 阿里云 SLB 900s 空闲超时的 2 倍)+ 客户端 Lettuce ping 120s 保活 + 应用层对 ConnectionException 做 1 次自动重试(带幂等键):
- 错误率 0,连续 30 天无 Connection reset
- 关键是三层兜底:服务端 timeout > SLB 空闲超时 > 客户端 ping 间隔,形成梯度
结论:没有银弹。Jedis 的 testOnBorrow 能兜住 90% 场景,但 evict 单线程是结构性瓶颈;Lettuce 的 ping 机制更优雅但 Sentinel 切换窗口仍需应用层重试兜底。最终方案是三层梯度 + 应用层重试,而不是单点参数。