1. 凌晨3点的告警:TPS从2000跌到200
那是一次大促前的压测,我们的订单服务在流量涨到平时3倍时突然"哮喘"------TPS从2000暴跌到200,Redis平均响应时间突破2秒。更诡异的是,重启服务后一切正常,但半小时后必然复现。
你猜这时候运维在群里甩了什么截图?Redis客户端的连接数从默认的8个暴涨到1000+,而监控显示实际活跃请求只有几十。
2. 为什么连接会泄漏?
先看一段典型的"错误示范"。这是我们项目里一个"人畜无害"的Redis工具类:
java
public class RedisUtils {
private static JedisPool pool = new JedisPool("redis-prod"); // 全局唯一连接池
public static String get(String key) {
Jedis jedis = pool.getResource(); // 关键点:这里借了连接
try {
return jedis.get(key);
} finally {
if (jedis != null) {
jedis.close(); // 坑在这里!
}
}
}
}
- 致命细节在于
jedis.close()的语义*。你以为它真会关闭连接?实际上:
- 健康连接 :调用
close()会把连接返还给池 - 异常连接 (比如网络闪断):
close()会直接销毁连接 ,而池子会补充新连接
当你的网络不稳定时,每次异常都会导致池子里的连接越来越少,最终触发池子的补偿机制疯狂创建新连接------这就是我们看到的1000+连接的真相。
3. 正确的关闭方式:用try-with-resources
Java 7+的解决方案是让资源自己决定生死:
java
public static String getFixed(String key) {
try (Jedis jedis = pool.getResource()) { // 自动调用close()
return jedis.get(key);
}
}
Jedis实现了AutoCloseable接口,它的close()逻辑是:
- 如果当前连接健康:标记为"可用"并归还给池
- 如果连接异常:默默记录metric后销毁
- 这才是连接池的标准用法*------让资源对象自己处理生命周期,而不是手动干预。
4. 数据对比:泄漏 vs 修复
我们用JMeter模拟了当时的场景:
| 场景 | 连接数峰值 | 平均响应时间 | 错误率 |
|---|---|---|---|
| 未修复(手动close) | 1243 | 2100ms | 23% |
| 修复后 | 12 | 28ms | 0% |
最离谱的是,旧代码在持续压测30分钟后,Redis的TCP端口被耗尽,彻底拒绝新连接------这就是为什么重启能暂时恢复。
5. 避坑清单:连接池的隐藏规则
- 不要复用连接对象 :比如把
Jedis实例放到ThreadLocal里。连接池的借还机制依赖close()调用,复用会导致计数混乱 - 警惕"默默重试":某些客户端在连接异常时会自动重试,如果没设超时,可能挂起整个线程池
- 监控必须加 :至少采集
active|idle|waiting连接数,像HikariCP的getMetrics()就能暴露泄漏 - 注意DNS缓存:连接池里的长连接可能缓存DNS解析结果,极端情况下会导致全集群流量倾斜
6. 最后一道防线:池子参数怎么调
如果你的服务真遇到连接泄漏,这两个参数能帮你顶住第一波:
properties
# 控制池子最大扩容速度(默认无限制)
redis.pool.maxTotal=50
# 借连接时的阻塞超时(默认永远等待)
redis.pool.maxWaitMillis=3000
但记住------参数只能缓解症状,代码才是根治方案。
那次事故后,我在团队Wiki上加了一条铁律:所有资源申请必须用try-with-resources,PR里看到手动close()直接打回。
你有遇到过更诡异的连接池问题吗?评论区聊聊你的"战斗伤痕"。