一次排查三种连接泄漏模式,再也不怕 HikariCP 连接池爆满了

叙事框架:现象 → 排查过程 → 根因 → 修复 → 预防

问题现象

干货分享:连接泄漏是 Spring Boot 项目里最难排查的问题之一,症状和连接数不够太像了,很多人第一反应是加 maximumPoolSize。但如果根因是泄漏,加大池大小只是延缓崩溃而已。最近我们团队就遇到了一起教科书式的泄漏事故------三个连接池同时爆满,但 HikariCP 的泄漏检测一开,47 次警告精准定位到三个方法。每个方法的泄漏模式各不相同:第一种是 SQL 执行卡住(慢查询无限等待),第二种是事务内调 HTTP 接口(事务不结束连接不释放),第三种是资源未关闭(忘了在 finally 中 close)。三种场景 jstack 线程栈特征完全不同。这篇文章把三种模式的代码案例、线程特征和定位技巧都整理好了,建议收藏。

排查过程

第一步:看连接

三个数据源------订单库(:3306)、库存库(:3307)、物流库(:3308)------全部 ESTABLISHED 占满。

每个池 maximumPoolSize=10,三个池一共 30 个连接全被占用。一个不剩。

第二步:看日志

翻应用日志,看到大量连接获取超时异常,以及 HikariCP 的 leak-detection-threshold 触发警告:

bash 复制代码
Caused by: java.sql.SQLTransientConnectionException: HikariPool-1 (order-ds) - Connection is not available, request timed out after 10000ms (total=10, active=10, idle=0, waiting=52)

还有 HikariCP 自身的泄漏检测:

bash 复制代码
WARN  - Connection leak detection triggered for connection 192.168.10.50:3306/order-db
Stack trace follows:
java.lang.Exception: Connection leak detection: connection has been active for at least 15000ms
	at cn.opencao.order.service.OrderService.createOrder(OrderService.java:32)

泄漏检测已经触发了 47 次 ,分别落在 createOrder(32 次)、updateInventory(12 次)、createLogistics(3 次)三个方法上------正好是三个不同的泄漏场景。

第三步:jstack 看线程栈

用 jstack 看线程分布,三种不同的阻塞模式:

108 个 RUNNABLE 线程 中有大量卡在 socketRead0------连接从池中借出后,在 executeQuery 阶段就卡住了,说明连接压根没被归还。

45 个 TIMED_WAITING 线程 卡在 Thread.sleep------这是事务内调了外部 HTTP 接口,事务一直不提交,连接一直占着。

15 个 WAITING 线程 卡在 HikariPool.getConnection------这是后续请求排队等连接。

第四步:JMX 看运行时指标

通过 Actuator 看每个连接池的实时指标:

bash 复制代码
order-ds:      active=10/10  pending=52  timeout=4%
inventory-ds:  active=10/10  pending=48  timeout=5%
logistics-ds:  active=9/10   pending=56  timeout=3%

三个池全部爆满,156 个线程在排队。三处泄漏同时发作,形成叠加效应。

根因分析

泄漏场景一:未关闭 JDBC 资源(createOrder)

java 复制代码
public void createOrder() {
    Connection conn = null;
    Statement stmt = null;
    ResultSet rs = null;
    try {
        conn = dataSource.getConnection();
        stmt = conn.createStatement();
        rs = stmt.executeQuery(query);
        while (rs.next()) {
            // 业务处理
        }
    } catch (SQLException e) {
        throw new RuntimeException("订单创建失败", e);
    }
    // ❌ 没有 finally 块关闭连接!
    // 查询异常 → 直接抛 → 连接永远不归还
}

最原始的写法------手动管理 JDBC 连接但忘了 close。当 executeQuerynext() 抛出异常时,connstmtrs 全部悬空,连接池不知道连接已可回收。

HikariCP 默认没有 leakDetectionThreshold,这类泄漏会在连接池默默积累,直到池满才发现。

泄漏场景二:事务内远程调用阻塞(updateInventory)

java 复制代码
@Transactional
public void updateInventory(String skuId, int quantity) {
    // 步骤1: 数据库扣减库存 --- 获取连接
    jdbcTemplate.update(sql, quantity, skuId, quantity);

    // 步骤2: 同步调用外部 WMS 系统 --- 阻塞 2-3 秒!
    // @Transactional 事务不提交,连接一直被占用
    restTemplate.postForObject(wmsUrl, request, String.class);
    // 只有方法返回时事务才提交,连接才归还
}

@Transactional 在方法入口开启事务(从池中借出连接),方法返回时才提交事务(归还连接)。如果在事务中间调了外部 HTTP 接口,连接会一直被占用直到 HTTP 响应返回。

一个请求等 2-3 秒,10 个连接最多扛 3-5 个并发。高峰期 TPS 远超这个数。

泄漏场景三:异常路径未归还连接(createLogistics)

java 复制代码
public void createLogistics(Order order) {
    Connection conn = null;
    try {
        conn = dataSource.getConnection();
        // ... 业务处理
        if (!addressValidator.isValid(order.getAddress())) {
            throw new IllegalArgumentException("地址不在配送范围");
        }
    } catch (Exception e) {
        log.error("创建物流单失败", e);
        throw e;  // ❌ 直接抛异常,没有 finally 释放连接!
    }
    // ❌ 连接从池中借出但从未归还
}

这个更隐蔽------大部分请求走正常路径时 conn 会被正确关闭,但地址校验失败这个异常路径上,catch 里直接 throw,没有 finally 块来释放连接。每触发一次这种异常,就漏掉一个连接。

三处叠加效应

三个泄漏同时存在于同一个服务的不同方法中:

场景 泄漏速率 每个泄漏占连接时间
未关闭资源 100% 泄漏 直到 maxLifetime(10min)
事务内远程调用 请求级阻塞 2-3 秒/次
异常路径未归还 异常时泄漏 直到 leakDetectionThreshold(15s)

连接池一共 30 个连接。正常请求每秒 200+ TPS,三个泄漏加起来,每 10 秒漏掉一堆连接。2 小时后池满。

修复方案

修复一:try-with-resources 自动关闭

java 复制代码
try (Connection conn = dataSource.getConnection();
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery(sql)) {
    // 业务处理
} // 自动 close,连接归还到池

修复二:事务内异步化远程调用

java 复制代码
public void updateInventory(String skuId, int quantity) {
    jdbcTemplate.update(sql, quantity, skuId, quantity);
    // MQ 异步通知 WMS,不阻塞事务
    rabbitTemplate.convertAndSend("wms.sync", new WmsSyncEvent(skuId, quantity));
}

去掉 @Transactional 只保数据操作,远程调用改成 MQ 异步投递。

修复三:finally 保证归还

java 复制代码
} catch (Exception e) {
    log.error("创建物流单失败", e);
    closeQuietly(conn, ps, null);  // 异常路径也释放
    throw e;
}

完整 Git diff:

部署修复

重新部署 v2.1 后,三个 Pod 依次滚动更新上线。

验证结果

部署后连接池全面恢复:

  • activeConnections 从 29 降到 6
  • pending 从 156 降到 0
  • 超时次数归零
  • 错误率归零
  • p99 响应恢复

避坑建议

1. 永远用 try-with-resources

Java 7+ 提供的 try-with-resources 是防御连接泄漏的第一道防线。手动 finally { close() } 容易遗漏,特别是在多个资源嵌套时。

2. @Transactional 里别调远程

事务连接是宝贵的池资源。事务内做 HTTP 调用、RPC、sleep------统统不要。事务只负责数据库操作,外部交互放外面或走 MQ 异步。

3. 配置 leakDetectionThreshold

yaml 复制代码
spring:
  datasource:
    hikari:
      leak-detection-threshold: 15000  # 连接借出超过 15 秒触发警告

连接泄漏的黄金信号。开启后,被占用的连接超时会在日志输出完整的 stack trace,精确定位泄漏代码行。

4. Actuator + Prometheus 监控连接池

yaml 复制代码
management:
  endpoints:
    web:
      exposure:
        include: metrics,prometheus

需要监控的关键指标:

指标 告警阈值 说明
hikaricp.connections.active > 80% maxPoolSize 连接利用率
hikaricp.connections.pending > 0 有请求在排队等连接
hikaricp.connections.timeout > 0 连接获取超时(紧急)

5. 代码审查关注连接生命周期

Review 清单:

  • JDBC 操作是否用了 try-with-resources?
  • @Transactional 方法内有无外部调用?
  • 异常路径是否确保资源释放?
  • 自定义线程池的连接是否在 finally 中归还?

附:完整命令清单

系统连接排查

bash 复制代码
ss -ant 'dport = :3306'                 # 查看某端口连接数
ss -ant 'dport = :3307 or dport = :3308' # 多端口
ss -ant | awk '{print $6}' | sort | uniq -c | sort -rn  # 连接状态统计

应用日志排查

bash 复制代码
grep 'Connection is not available' app.log | head
grep 'Connection leak detection' app.log | head
grep -c 'Connection leak detection' app.log              # 泄漏触发次数

JVM 线程分析

bash 复制代码
jstack <pid> | grep 'http-nio' | wc -l                  # 活跃线程数
jstack <pid> | grep 'java.lang.Thread.State' | sort | uniq -c | sort -rn  # 线程状态分布
jstack <pid> | grep -A 30 'http-nio-8080-exec-1'        # 看具体线程栈

HikariCP 运行时指标

bash 复制代码
curl -s http://localhost:8080/actuator/metrics/hikaricp.connections.active
curl -s http://localhost:8080/actuator/metrics/hikaricp.connections.pending
curl -s 'http://localhost:8080/actuator/metrics/hikaricp.connections.active?tag=pool:order-ds'

连接池配置检查

bash 复制代码
grep -rn 'leak-detection-threshold\|maximum-pool-size\|connection-timeout' src/main/resources/
相关推荐
咪库咪库咪1 小时前
Cypher入门
后端
雪隐2 小时前
个人电脑玩AI-08让5060 Ti给你打工——我拿 Unlimited-OCR扫了 600 页书,然后悟了
人工智能·后端
AskHarries2 小时前
用 OpenClaw 做一份完整 PPT:从主题、提纲到 slide deck
后端·程序员
Csvn2 小时前
Linux 常用操作命令合集与运维实战
后端
卷无止境3 小时前
现代C++ 编译器生态及其对编程规范的影响
后端
云技纵横3 小时前
一个 @Async,把 @Transactional 的事务边界打穿了
后端·面试
BothSavage3 小时前
OpenHarness源码研究-3-codex配置到输出对话
后端·架构
SimonKing3 小时前
Google第三方授权登录
java·后端·程序员