数据库连接池泄漏排查:一条 getConnection 忘 close 的蝴蝶效应
引言
凌晨两点,告警短信把运维叫醒:order-service HikariPool-1 - Connection is not available, request timed out after 30000ms。登上服务器一看,20 个数据库连接全部 active,但数据库 processlist 里一半连接在 Sleep、没有任何 SQL 在执行——连接被借走了,却没有在跑查询,也没有归还。重启服务后一切恢复正常,但第二天同一时间,告警再次响起。
这是典型的连接泄漏(Connection Leak):代码从池里借走连接后,因为异常分支、提前 return、手动取连接等原因没有 close,连接永远不会回到池中。它和"慢 SQL 占满连接池"症状相似但根因完全不同——慢 SQL 是连接在用但用得久(processlist 能看到正在执行的语句),泄漏是连接"人间蒸发"(processlist 显示 Sleep,池却显示 active)。更阴险的是它的累积特性:代码每天只泄漏两三个连接,但池总共只有 20 个,一周后池就被抽空,而且时间点毫无规律,重启只能续命。这篇文章完整复盘一次真实泄漏的排查过程:HikariCP 配置如何提前发现、metrics 和线程栈如何定位、try-with-resources 如何根治,以及如何用 Code Review 清单和静态检查让泄漏再也进不了代码库。
一、先分清:连接池耗尽的三种根因
告警都是"Connection is not available",但处置方向完全不同,第一步必须判型:
| 根因 | 池指标特征 | DB processlist 特征 | 时间特征 |
|---|---|---|---|
| 慢 SQL 占满 | active 满,等待线程多 | 大量 Sending data/Locked,SQL 执行时间长 | 与慢查询/大促时间吻合 |
| 流量打满 | active 满,请求 QPS 确实高 | SQL 都在正常快速执行 | 与流量曲线一致,扩容可缓解 |
| 连接泄漏 | active 只增不减,与流量无关 | 泄漏连接多为 Sleep,Time 很长(小时级) | 缓慢累积、夜间也涨、重启清零后复发 |
本次现场的关键证据:
HikariPool-1 - Pool stats (total=20, active=20, idle=0, waiting=47)
# 20 个连接全 active,47 个线程排队等连接
# MySQL processlist
id user time command state info
8821 app 48213 Sleep NULL NULL ← 借走 13 小时,没有任何 SQL
8835 app 37120 Sleep NULL NULL
8840 app 20566 Sleep NULL NULL
...
8901 app 2 Query Sending data SELECT ... ← 正常请求反而在排队
time=48213 秒的 Sleep 连接就是铁证:正常业务请求毫秒级完成连接借还,连接在 Sleep 状态挂 13 小时,只能是有人借了没还。
泄漏的蝴蝶效应链
一个 catch 分支里漏了 close
→ 该连接永不归还(池中 idle 永久 -1)
→ 每天泄漏几个,active 基线缓慢抬升
→ 某天高峰期 active 撞到 maximumPoolSize
→ 新请求 getConnection 排队,connectionTimeout 30s 后抛异常
→ Tomcat 线程大批 WAITING 在 HikariPool.borrow
→ Nginx 30s 收不到响应 → 502(与 502 篇同一雪崩下游)
→ 重启后连接全部释放 → 几天后再次抽空
二、第一道防线:让 HikariCP 自己喊出泄漏
2.1 leakDetectionThreshold:生产环境就该常开的神器
HikariCP 内置连接泄漏检测:连接借出时间超过阈值仍未归还,打印借出时刻的完整调用栈。
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 10
connection-timeout: 3000
# ★ 连接借出超过 10 秒未归还,打印泄漏告警(含借连接的堆栈)
leak-detection-threshold: 10000
max-lifetime: 1800000
keepalive-time: 300000
阈值怎么定:取正常请求持有连接时间 P99 的 3~5 倍。普通 OLTP 请求连接持有通常 <100ms,设 10 秒几乎零误报;批量任务/报表持有时间长,要么设大阈值(如 60 秒),要么走独立连接池。
触发后的日志长这样(这就是定位的核心证据):
2026-09-18 14:22:31 WARN ProxyLeakTask - Connection leak detection triggered
for conn42: pgrap... , stack trace follows
java.lang.Exception: Apparent connection leak detected
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128)
at com.xxx.report.ImportRecordServiceImpl.importRecords(ImportRecordServiceImpl.java:87) ★
at com.xxx.report.ImportController.upload(ImportController.java:44)
...
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(...)
堆栈直指 ImportRecordServiceImpl.java:87——谁借的、从哪个 HTTP 请求借的,一目了然。注意:打印告警不代表连接一定泄漏,只是"借太久了",真正的慢 SQL 也会触发——但堆栈仍然告诉你哪个业务在长时间持有,两种问题都值得查。
2.2 配套的池参数(防雪崩,不替代根治)
spring:
datasource:
hikari:
connection-timeout: 3000 # 借不到连接 3 秒快速失败,别让线程无限排队
validation-timeout: 1000
maximum-pool-size: 20 # 小而快,不要靠调大池子掩盖泄漏
minimum-idle: 10
# 连接最大寿命:防止持有陈旧连接(泄漏连接到寿命也会被强制回收,是一层兜底)
max-lifetime: 1800000
connection-test-query: SELECT 1
两个认知纠偏:
- 调大 maximum-pool-size 不解决泄漏——池子 20 变 200,只是把"抽空时间"从一周推迟到两个月,故障照样来,而且 DB 侧连接压力放大 10 倍。
- max-lifetime 是兜底不是解药:泄漏连接到寿命被强制关闭时,持有它的线程下次使用会拿到异常("connection is closed"),可能引发更诡异的报错。泄漏必须在代码层根治。
三、第二道防线:metrics 让泄漏趋势在告警前可见
泄漏是慢性病,光靠日志堆栈(单点证据)不够,还要有趋势指标证明"连接只借不还"。
3.1 暴露 HikariCP 指标
Spring Boot 下 HikariCP 指标自动接入 Micrometer,暴露给 Prometheus:
management:
endpoints:
web:
exposure:
include: prometheus,health,metrics
metrics:
tags:
application: order-service
核心指标:
| 指标 | 含义 | 泄漏时的表现 |
|---|---|---|
hikaricp_connections_active | 已借出连接数 | 凌晨低峰仍持续上涨不回落 |
hikaricp_connections_idle | 空闲可借连接数 | 缓慢降到 0 |
hikaricp_connections_pending | 等待借连接的线程数 | 平时 0,池满时飙升(业务受损的直接信号) |
hikaricp_connections_timeout_total | 借连接超时累计次数 | >0 说明已经在伤人了 |
hikaricp_connections_usage_seconds_max | 连接被持有最长时间 | 出现小时级值 = 泄漏实锤 |
3.2 告警规则:区分"忙"和"漏"
关键思路是用低峰期的 active 连接数区分两种情况——真业务繁忙,凌晨低峰 active 会随流量回落;泄漏不会。
# ① 致命:已经有请求借不到连接(业务在报错)
- alert: HikariPendingConnections
expr: hikaricp_connections_pending{application="order-service"} > 0
for: 30s
labels: {severity: P0}
# ② 预警:低峰期(凌晨2-5点)active 仍然很高且不降 → 泄漏的典型趋势
- alert: HikariActiveConnectionsNotDroppingAtNight
expr: |
avg_over_time(hikaricp_connections_active{application="order-service"}[10m]) > 10
and hour() >= 2 and hour() <= 5
for: 10m
labels: {severity: P1}
# ③ 实锤:单连接持有时间超过 5 分钟(正常请求不可能)
- alert: HikariConnectionHeldTooLong
expr: hikaricp_connections_usage_seconds_max{application="order-service"} > 300
for: 10s
labels: {severity: P1}
# ④ 池使用率持续 100%
- alert: HikariPoolSaturated
expr: |
hikaricp_connections_active / hikaricp_connections_max >= 1
for: 1m
labels: {severity: P1}
告警 ② 是发现"慢性泄漏"最实用的一条:它在池被真正抽空、用户受影响之前几天就能发出预警。
3.3 Grafana 面板上一眼看出泄漏
健康池的 active 曲线是"随流量起伏的锯齿"——涨上去、请求完成后立刻掉回来。泄漏池的曲线是"只涨不跌的台阶"——每过一个泄漏点,基线抬高一级,再也不回落。看一天的 active 曲线形态,比任何单点指标都直观。
四、定位:线程堆栈与 DB 现场双向印证
leakDetectionThreshold 日志通常已经直接给出代码行。如果阈值没开(本次事故就是上线后才补的配置),用以下方法事后定位。
4.1 现场抓线程栈
# 池满时刻连抓 3 次,间隔 5 秒
for i in 1 2 3; do
jstack -l <pid> > /tmp/pool_$i.txt
sleep 5
done
但注意:泄漏的连接此时没有线程在操作它(借走它的线程早就在某个异常分支结束了),所以 jstack 里找不到"正在使用泄漏连接"的线程。jstack 在这个场景的用途是看等待队列确认池满现象,真正的定位要靠下面两招。
4.2 用 processlist 反推泄漏时间和业务
SELECT id, user, db, command, time, state, LEFT(info,100)
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 60
ORDER BY time DESC;
看 Sleep 连接的 time(存活秒数):如果一批连接的存活时间集中在每天的某个定时任务、某次批量导入、某个报表导出的时间点,就圈定了嫌疑功能。本次事故 48213 秒(约 13 小时)正好对应前一天下午 3 点的一次 Excel 导入——与 ImportRecordServiceImpl 高度吻合。
进一步确认连接从哪个客户端来:
-- 看连接的来源 IP/端口,再回应用服务器用 netstat 对应到具体线程
SELECT id, host, time FROM information_schema.processlist WHERE command='Sleep';
4.3 终极手段:临时开 leakDetectionThreshold 蹲守
已经在线上但没开检测时,可以动态调整(Spring Boot 配置中心热刷新,或写个临时端点):
@RestController
public class HikariDebugController {
private final HikariDataSource dataSource;
@PostMapping("/debug/hikari/leak/{ms}")
public String setLeakThreshold(@PathVariable long ms) {
dataSource.setLeakDetectionThreshold(ms); // 动态打开,蹲到即改回 0
return "leakDetectionThreshold=" + ms;
}
}
设置后等业务复现(让用户/定时任务再走一次嫌疑功能),泄漏堆栈必现。定位完记得关闭临时端点并恢复阈值。
4.4 本次事故的问题代码
// ❌ 事故代码:手动取连接,异常分支跳过了 close
public void importRecords(MultipartFile file) throws Exception {
Connection conn = dataSource.getConnection(); // 第 87 行:借连接
PreparedStatement ps = null;
try {
ps = conn.prepareStatement("INSERT INTO import_record ...");
List<Record> records = parseExcel(file);
for (Record r : records) {
bindAndBatch(ps, r);
}
ps.executeBatch();
conn.commit();
ps.close();
conn.close(); // 正常路径关了
} catch (Exception e) {
log.error("导入失败", e);
uploadErrorFile(file); // ★ 异常时走这里:抛异常/return 了
throw e; // ps、conn 全部没关!连接永久泄漏
}
}
问题清单:
parseExcel在getConnection之后执行——光解析 Excel 就要好几秒,这期间连接白借白占;- 异常分支只记日志+抛异常,没有 finally,
conn.close()被跳过; ps.close()和conn.close()分开手写,正常路径漏一个都不完整;- 自动提交被关过(用了 conn.commit()),泄漏的连接还带着未提交事务,DB 侧可能持有锁,危害加倍。
五、修复:try-with-resources 让"归还"成为语法保证
5.1 标准修复
// ✅ 修复后:try-with-resources,任何分支(正常/异常/return)都保证关闭
public void importRecords(MultipartFile file) throws Exception {
// 先做不占连接的准备工作(Excel 解析、校验)
List<Record> records = parseExcel(file);
validate(records);
// 连接只在真正访问 DB 期间持有
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(INSERT_SQL)) {
conn.setAutoCommit(false);
for (Record r : records) {
bindAndBatch(ps, r);
}
ps.executeBatch();
conn.commit();
} catch (BatchUpdateException e) {
// try-with-resources 关闭连接时若有未提交事务,JDBC 规范要求自动 rollback
log.error("批量导入失败,已回滚", e);
uploadErrorFile(file);
throw new BizException("IMPORT_FAILED", "导入失败");
}
// 无论走哪条路,conn/ps 都已自动 close(归还连接池)
}
三个关键改动:
- try-with-resources:Connection、PreparedStatement 都实现 AutoCloseable,离开 try 块必然 close,编译器保证,不依赖人的细心;
- 连接获取时机后移:先解析 Excel/校验,再借连接,连接持有时间从"整个导入流程"压缩到"批量写库"几秒;
- 异常语义清晰:失败回滚由 try-with-resources 关闭时自动完成,业务异常正常向上抛。
5.2 Spring 管理的资源:尽量不要手动 getConnection
本次事故的另一个根源是绕过框架手动借连接。绝大多数业务用 MyBatis/JdbcTemplate 就够了,框架会在语句执行完自动归还:
// ✅ 推荐:JdbcTemplate,连接由模板管理,无需手动 close
jdbcTemplate.batchUpdate(INSERT_SQL, batchArgs);
// ✅ MyBatis:SqlSession 由 Spring 管理,Mapper 方法执行完连接即还
mapper.batchInsert(records);
必须手动用 Connection 的场景(多语句批量、特殊事务控制、BLOB 流操作)遵守两条:① try-with-resources;② 连接持有区间内禁止 RPC、文件解析、sleep 等慢操作。
5.3 验证修复有效
- 触发正常导入和故意造异常(传一个坏文件)两种路径;
- 观察
hikaricp_connections_active:两种路径执行完都应立即回落到基线; - 观察
usage_seconds_max:不再出现秒级以上持有; - 连续跑 100 次异常导入,池指标纹丝不动即根治。
六、预防:让泄漏在合并代码前被拦住
故障修复只救了一次,机制建设才能防住下一次。
6.1 Code Review 检查清单(贴到团队 PR 模板)
数据库资源检查项:
[ ] 代码里没有裸的 dataSource.getConnection() / DriverManager.getConnection()
——优先用 JdbcTemplate/MyBatis;确需手动获取必须 try-with-resources
[ ] Connection / Statement / ResultSet / InputStream 等资源
全部在 try-with-resources 中声明,禁止手写 close()
[ ] 不存在"先 close 再在后续分支可能跳过 close"的分散关闭写法
[ ] getConnection 之后没有文件解析、RPC、Thread.sleep 等慢操作
——连接持有区间内只做 DB 访问
[ ] 异常分支(catch 里 return/抛异常)也经过资源关闭路径
[ ] @Transactional 方法内没有把 Connection 传到方法外长期持有
[ ] 批量任务/定时任务有独立连接池或明确的连接持有超时评估
6.2 静态检查:让机器自动拦
SpotBugs + Find Security Bugs(之前安全工具篇装过)内置资源未关闭规则:
<!-- SpotBugs 检测资源泄漏(ODR 系列规则) -->
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<visitors>
<visitor>ODR_OPEN_DATABASE_RESOURCE</visitor>
<visitor>OBL_UNSATISFIED_OBLIGATION</visitor>
</visitors>
</configuration>
</plugin>
相关规则:
| 规则 | 含义 |
|---|---|
| ODR_OPEN_DATABASE_RESOURCE | 方法打开的 DB 资源在所有路径上未关闭 |
| OBL_UNSATISFIED_OBLIGATION | 资源关闭义务未在所有异常路径满足 |
| RV(try-with-resources 建议) | 可配合 IDE 检查统一写法 |
IDEA 内置检查(Settings → Editor → Inspections):
- Resource management → "Resource may be closed" / "AutoCloseable used without try-with-resources" 开为 Error 级别;
- 写 PR 时 IDEA 就会在裸 getConnection 处标黄,编码期拦截。
ArchUnit 架构建模(进阶):禁止业务包直接依赖 DataSource,把手动借连接收敛到少数基础设施类:
@ArchTest
static final ArchRule 业务代码不得直接获取连接 =
noClasses().that().resideInAPackage("..service..")
.should().dependOnClassesThat().haveFullyQualifiedName("javax.sql.DataSource");
6.3 上线基线:三个一
- 一个常开配置:
leak-detection-threshold按 P99×3~5 设入生产配置模板,新项目脚手架默认带; - 一组默认告警:pending>0 / 长持有 / 低峰 active 不降三条进团队 Prometheus 规则模板;
- 一道 CI 门禁:SpotBugs ODR 规则在流水线阻断合并。
七、常见问题
7.1 用了 @Transactional 还会泄漏连接吗?
正常情况下不会——Spring 的事务同步管理器在事务方法结束(提交或回滚)时通过 DataSourceUtils 自动归还连接,开发者根本不碰 close。但三种情况会出问题:① 在事务方法内手动 dataSource.getConnection() 取了一个不被 Spring 管理的连接(必须自己关,这是高频事故源);② 事务方法里发起长时间 RPC 导致连接随事务被长时间持有(不是泄漏但效果类似,属于"长事务占连接");③ 在方法外保存了从事务同步器取出的 Connection 引用。规则:@Transactional 内只用 Mapper/JdbcTemplate,不要手动取连接。
7.2 连接泄漏和连接池"看起来满"怎么最快区分?
三步:① 看流量——低峰期 active 是否依然只涨不跌(泄漏);② 查 processlist——Sleep 且 time 达小时级(泄漏)vs 长时间 Query(慢 SQL);③ 看 usage_seconds_max——出现分钟/小时级持有基本实锤。拿不准直接看 leakDetectionThreshold 日志的借出堆栈,那是确定性证据。
7.3 线程池里的任务异常没捕获,会导致连接泄漏吗?
会,这是仅次于 catch 漏关的第二大场景:@Async/线程池任务里从 Mapper 之外手动拿了连接,任务抛出未捕获异常后线程死亡归还线程池,但手动借的连接没人关。修复双管齐下:① 连接一律 try-with-resources;② 线程池配统一异常处理器(@Async 篇的 AsyncUncaughtExceptionHandler),让任务异常可见可告警。资源关闭不能依赖"线程结束后连接会自动回收"——那可能要等到 max-lifetime(30 分钟后),期间连接就是泄漏状态。
7.4 leakDetectionThreshold 设成 0(关闭)是不是性能最好?
检测开销极小(借出时起一个延时任务,归还时取消),正常请求完全无感,生产环境没有理由关闭。它唯一的"噪声"是长持有合法场景(大批量导入、存储过程)会打印堆栈——正确做法是给这类场景单独连接池并设独立阈值,而不是全局关闭检测。把它类比成安全带:不能因为大多数行程用不上就不系。
7.5 多数据源/动态数据源场景怎么排查?
动态路由数据源(如 @DS、AbstractRoutingDataSource)下要注意:泄漏检测要配置在每个真实目标池上而不是路由数据源上;指标会带 pool 名标签,按池分别建告警。常见坑还有:切换数据源的 AOP 在异常时没清理 ThreadLocal 绑定,导致后续请求拿到错误池的连接——这属于数据源路由 bug 而非连接泄漏,现象是"连接在错误的业务库里 Sleep",通过 processlist 的 db 列与请求预期不符来识别。
7.6 连接泄漏到 max-lifetime 被强制关闭后,池里的计数会不会错乱?
不会错乱,HikariCP 会把超过 max-lifetime 的连接物理关闭并从池里摘除,池计数恢复一致——这是它作为兜底的价值。但代价前面说过:正在持有该连接的业务线程再用它执行语句时会抛 "This connection has been closed" / "Connection is closed" 之类异常,表现为零星诡异报错。所以 max-lifetime 只能兜底,不能当作泄漏处理机制;而且默认 max-lifetime 30 分钟意味着泄漏连接最多半小时才被回收,对一个 20 连接的池来说完全等不起。
八、总结
排查与预防速查卡
判型(processlist):
Sleep + time 小时级 + 低峰 active 只涨不跌 = 泄漏
Query + Sending data 长时间 = 慢 SQL
active 随流量正常起伏 = 真忙
定位:
leak-detection-threshold=10000 打印借出堆栈(首选,生产常开)
processlist Sleep 时间反推业务 → jstack 看等待队列 → 临时端点蹲守
修复:
try-with-resources 管 Connection/PS/RS;先解析后借连接
优先 JdbcTemplate/MyBatis,不裸 getConnection
预防:
配置:leak-detection-threshold 入脚手架
监控:pending>0(P0)/ 长持有 5min(P1)/ 低峰 active 不降(P1)
门禁:SpotBugs ODR + IDEA 资源检查 + PR 清单
架构:ArchUnit 禁止 service 层直接依赖 DataSource
记住:调大池和 max-lifetime 都是延缓不是修复,根因永远是漏 close
一句话
连接池耗尽的告警千篇一律,但底下藏着三种完全不同的根因,排查的第一步永远是判型而不是重启:processlist 里全是长时间 Query 是慢 SQL,active 随流量起伏是真忙,而大批 time 长达小时级的 Sleep 连接、低峰期 active 曲线只涨不跌成台阶状,就是连接泄漏——有人通过 getConnection 把连接借了出去,却在异常分支、提前 return 或手写关闭的遗漏中永远没有 close,它不像慢 SQL 那样当场爆发,而是以每天两三个的速度抽空一个 20 连接的池,一周后在某个高峰期让 47 个线程排队、Nginx 502、全链路雪崩,重启清零、几天后复发,是最典型的慢性病。定位这个"蝴蝶"不需要玄学:HikariCP 的 leak-detection-threshold 按正常持有 P99 的三到五倍(OLTP 通常 10 秒)常开,连接借出超时未还时它会直接打印借出时刻的完整调用栈,类名行号一步到位;指标侧用 hikaricp_connections_active/idle/pending/usage_seconds_max 四条曲线建告警,其中"凌晨低峰 active 仍持续不回落"是比业务报错早好几天的预警信号,没开检测时还能用 processlist 的 Sleep 时长反推嫌疑功能、动态打开阈值蹲守堆栈。根治只有一个原则——资源关闭必须由语法而不是人的细心保证:try-with-resources 让 Connection、PreparedStatement、ResultSet 在任何正常或异常路径上都自动 close,把 Excel 解析、RPC 这类慢操作挪到借连接之前以缩短持有时间,业务代码一律走 JdbcTemplate/MyBatis 让框架管理连接,不裸调 getConnection;而团队级的根治是三道前置防线——PR 模板里的资源关闭检查清单、SpotBugs 的 ODR_OPEN_DATABASE_RESOURCE 静态门禁、IDEA 的 AutoCloseable 检查,进阶再用 ArchUnit 直接禁止 service 层依赖 DataSource,把手动借连接收敛到少数基础设施类。最后记住两个"没用的自救":调大 maximumPoolSize 只会把抽空从一周推迟到两个月并放大数据库压力,max-lifetime 强杀泄漏连接是兜底但会制造零星的 connection closed 诡异报错——池化资源泄漏的正确处理永远是在代码里让每个借出去的连接都有一个语法保证的归还终点,而不是让池和数据库替你健忘的代码擦屁股。
给团队的建议
| 项 | 建议 |
|---|---|
| 配置 | leak-detection-threshold=P99×3~5 写入所有服务脚手架,生产常开 |
| 监控 | pending/长持有/低峰不降三条告警进规则模板 |
| 编码 | 资源一律 try-with-resources;先准备后借连接;优先框架数据访问 |
| Review | PR 模板内置资源关闭清单,裸 getConnection 必问 |
| CI | SpotBugs ODR 规则阻断合并,IDEA 检查设 Error |
| 架构 | 业务层不直接依赖 DataSource;批量任务独立连接池 |
| 应急 | 判型三看(流量/processlist/持有时间),重启只能临时止血 |
互动话题:你们遇到过连接泄漏吗?最后发现的漏 close 代码藏在什么奇葩分支里?定时任务还是文件导入?评论区聊聊你的排查经历。
参考资料
- HikariCP 官方 Wiki:Configuration(leakDetectionThreshold 等参数)
- HikariCP 官方 Wiki:Metric Dropwizard/Micrometer 指标说明
- Oracle JDBC 规范:try-with-resources 与未提交事务的自动回滚
- SpotBugs Bug Description:ODR_OPEN_DATABASE_RESOURCE
- Spring Framework 文档:DataSource 资源与事务同步管理
标题:数据库连接池泄漏排查:一条 getConnection 忘 close 的蝴蝶效应
作者:jiangyi
地址:http://jiangyi.space/articles/2026/09/25/1789829020926.html
公众号:服务端技术精选
- 引言
- 一、先分清:连接池耗尽的三种根因
- 泄漏的蝴蝶效应链
- 二、第一道防线:让 HikariCP 自己喊出泄漏
- 2.1 leakDetectionThreshold:生产环境就该常开的神器
- 2.2 配套的池参数(防雪崩,不替代根治)
- 三、第二道防线:metrics 让泄漏趋势在告警前可见
- 3.1 暴露 HikariCP 指标
- 3.2 告警规则:区分"忙"和"漏"
- 3.3 Grafana 面板上一眼看出泄漏
- 四、定位:线程堆栈与 DB 现场双向印证
- 4.1 现场抓线程栈
- 4.2 用 processlist 反推泄漏时间和业务
- 4.3 终极手段:临时开 leakDetectionThreshold 蹲守
- 4.4 本次事故的问题代码
- 五、修复:try-with-resources 让"归还"成为语法保证
- 5.1 标准修复
- 5.2 Spring 管理的资源:尽量不要手动 getConnection
- 5.3 验证修复有效
- 六、预防:让泄漏在合并代码前被拦住
- 6.1 Code Review 检查清单(贴到团队 PR 模板)
- 6.2 静态检查:让机器自动拦
- 6.3 上线基线:三个一
- 七、常见问题
- 7.1 用了 @Transactional 还会泄漏连接吗?
- 7.2 连接泄漏和连接池"看起来满"怎么最快区分?
- 7.3 线程池里的任务异常没捕获,会导致连接泄漏吗?
- 7.4 leakDetectionThreshold 设成 0(关闭)是不是性能最好?
- 7.5 多数据源/动态数据源场景怎么排查?
- 7.6 连接泄漏到 max-lifetime 被强制关闭后,池里的计数会不会错乱?
- 八、总结
- 排查与预防速查卡
- 一句话
- 给团队的建议
- 参考资料
评论