Java 数据库连接池耗尽与连接泄漏调试实战:从告警到根因的完整闭环
技术主题:Java 编程语言
内容方向:具体功能的调试过程(连接池耗尽与连接泄漏排查)
引言
数据库连接池问题很“隐蔽”:没有明显的错误日志,却会以响应变慢、偶发超时开场,最后演变成池耗尽、全站雪崩。本文复盘一次生产事故,从“慢查询还是连接泄漏”的分辨开始,完整走一遍复现、定位与修复,并给出可直接落地的 HikariCP 配置与代码改造范式。
一、问题现象
- 峰值时段接口 P99 从 300ms → 5s+,偶发 504;
- 应用日志无异常堆栈,但数据库连接使用数接近 poolMaximumSize;
- 数据库侧 QPS 正常,慢 SQL 占比不高;
- jstack 显示部分线程卡在 getConnection / borrow 阶段等待。
初判:更像“连接回收不及时或泄漏”而非“数据库本身变慢”。
二、排查步骤
- 先区分“慢 SQL”与“拿不到连接”
- 打开 SQL 执行耗时与池获取耗时的分布日志;
- 将“获取连接等待时间”与“SQL 执行时间”分别打点到指标系统。
- 栈与上下文
- 在线抓取线程 dump,检索 HikariCP 的 getConnection 与 close 调用链;
- 对比业务线程是否存在 try-with-resources 缺失、异常分支未 close。
- 池与生命周期
- 检查 maxLifetime 是否明显大于数据库连接的服务端超时(导致半开连接);
- 验证 leakDetectionThreshold 是否开启并能打印栈。
三、解决思路(总览)
- 代码层:所有数据库访问统一采用 try-with-resources,确保异常路径也能关闭;
- 连接池层:开启泄漏检测、合理设置 maxLifetime、connectionTimeout、idleTimeout;
- 观测层:区分“borrow 等待时间”与“SQL 执行时间”,并对泄漏栈进行采样记录;
- 风险隔离:读写分离或热点表读走缓存,缓解峰值借用压力。
四、关键配置与代码片段
1) Spring Boot 下的 HikariCP 配置(application.yml)
1 | spring: |
要点:maxLifetime 要小于数据库侧的连接生存时间(如 MySQL wait_timeout)。leakDetectionThreshold 在问题定位期开启即可,避免日志过多。
2) 正确的资源关闭范式(try-with-resources)
1 | // 反例:异常时可能跳过 close() |
1 | // 正例:始终自动关闭,包含异常路径 |
3) 统一的 JDBC 访问封装,防重复代码与漏关
1 | public class JdbcTemplateLite { |
4) 记录“借连接等待时间”与“SQL 执行时间”
1 | long t0 = System.nanoTime(); |
5) 针对泄漏的栈采样与告警(基于 Hikari 日志)
- 启用 leak-detection 后,Hikari 会输出未归还连接的创建栈;
- 将包含“leak detection”关键词的日志转发到告警系统,按调用链与类名聚合;
- 关联到发布版本与接口名,快速定位到具体业务代码。
五、验证与效果
- A/B 灰度 3 天:
- 借连接等待 P95 从 1200ms → 80ms;
- SQL 执行 P95 变化不大(说明主要瓶颈在借用阶段);
- 连接池使用率稳定在 50%-70%,无长尾借用。
- 生产一周:
- 504 减少为 0;
- 无新增泄漏告警。
六、预防清单
- 统一 JDBC 访问封装并使用 try-with-resources;
- maxLifetime 小于数据库连接超时;
- 合理设置 connectionTimeout(2s-3s)+ 快速失败重试;
- 灰度开启 leakDetectionThreshold,定位后关闭或上调阈值;
- 关键接口引入读缓存/降级,削峰填谷;
- 指标分离:borrow 等待、SQL 执行、池使用率、超时/失败率。
总结
连接池问题的关键在于“把症状拆开看”:分清“借用等待”与“执行耗时”。通过代码范式(try-with-resources)、池参数(maxLifetime/connectionTimeout)与可观测性(借用/执行分离指标),可以快速复现场景、锁定根因并稳定恢复。以上配置与代码可直接落地,适合在生产环境逐步应用。