StackTraces
Spring Boot已验证

Spring Boot HikariPool Connection is not available

@debug_master32 天前更新阅读 6 min00

报 HikariPool-1 - Connection is not available 基本不是数据库挂了,是连接池被借空——有代码拿了连接不还,或者事务里包着慢 I/O 长时间占着连接。开 leak-detection-threshold 能把泄漏点连同调用栈打出来。

Issue & Env(案发现场与环境)

  • 业务场景:并发一上来,部分请求开始报连接超时;轻的只影响个别接口,重的整个应用所有用到 DB 的接口都无响应。
  • 环境:HikariCP(Spring Boot 2.x / 3.x 默认连接池),connectionTimeout 默认 30000ms(30 秒)。HikariCP 3.4.5 与 5.1.0 下均有 Connection is not available 的案例。

StackTrace(堆栈日志)

请求等满 30 秒拿不到连接,抛出的标准异常:

java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.
	at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696)
	at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:197)
	at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:186)
	at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:157)
	at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:124)
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:352)
	at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:465)
	at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:480)
	at org.springframework.jdbc.core.JdbcTemplate.queryForList(JdbcTemplate.java:573)
	at com.example.service.LeakService.doQuery(LeakService.java:42)
	at com.example.controller.OrderController.listOrders(OrderController.java:68)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:1004)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:750)

压到池底时日志会显示连接数全部耗尽:

HikariPool-1 - Connection is not available, request timed out after 30000ms (total=0, active=0, idle=0, waiting=3)

total=0, active=0, idle=0, waiting=3 表示池里一个可用连接都没有,3 个线程在排队干等。

Reproduce(复现代码)

写一个故意不归还连接的代码,把池耗光。默认 maximumPoolSize 是 10:

@Service
public class LeakService {
    private final JdbcTemplate jdbcTemplate;

    public LeakService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    // 借出连接后不关闭,模拟泄漏
    public void leak() throws SQLException {
        jdbcTemplate.getDataSource().getConnection(); // 用完不 close()
    }
}

并发调 leak() 超过池大小后,后续任何请求都会在 getConnection() 处卡满 30 秒,然后抛上面那段超时异常。

Root Cause(根因分析)

HikariCP 的连接池大小是固定的(maximum-pool-size,默认 10)。每个线程执行 SQL 前要先 borrow 一个连接,用完了 return 回去。如果某个方法借了连接却从不归还,或者事务迟迟不结束、连接一直被线程占着,池子就被慢慢掏空。

掏空后,新来的 getConnection() 只能排队等空闲连接,等到 connectionTimeout(默认 30 秒)还是等不到,就抛出 SQLTransientConnectionException。这种故障有传染性:一个接口泄漏连接,量一大,别的纯读接口也会因为拿不到连接而被拖垮,最终整站雪崩。

最常见的泄漏来源是两类:

  1. 连接借了不还:手写 getConnection() 却不用 try-with-resources 关闭。
  2. @Transactional 里干慢活:事务全程持有连接,方法里又去调外部 HTTP 接口、发消息或做耗时计算,连接被白白占着几十秒。这是生产里最隐蔽的一种,因为代码看起来「没有一行忘关连接」。

HikariCP issue #2161、#2374 里还出现了另一种形态:网络抖动后连接总数掉到 0 且不自动重建,需重启应用。这种属于池的自愈边界,根因在连接创建线程卡在 socket 读操作上,和普通泄漏是两条线,不要混为一谈。

The Fix(解决方案)

第一步:开启泄漏检测,把元凶揪出来

spring:
  datasource:
    hikari:
      leak-detection-threshold: 5000   # 连接被借走超过 5 秒未归还就告警

开启后,泄漏的连接会在超时点打印调用栈,直接定位到是哪一行借的:

WARN com.zaxxer.hikari.pool.ProxyLeakTask - Connection leak detection triggered for connection org.postgresql.jdbc.PgConnection@1234abcd, stack trace follows
java.lang.Exception: Apparent connection leak detected
	at com.example.service.PaymentService.callExternalPaymentGateway(PaymentService.java:85)

第二步:修代码

借出来的连接用 try-with-resources 自动归还:

try (Connection conn = jdbcTemplate.getDataSource().getConnection()) {
    // 用 conn
}

@Transactional 里别做外网调用、重计算这类慢活,把外部 I/O 挪到事务外:

// 先做外部调用,再开事务落库
PaymentResult result = paymentClient.pay(request);
orderService.saveOrderWithResult(request, result);

第三步:按需调池参数

确认不是泄漏、单纯并发高,再调大池子或缩短等待时间:

spring:
  datasource:
    hikari:
      maximum-pool-size: 30
      connection-timeout: 10000
      keepalive-time: 30000

调大池子只是缓冲,治标不治本。池子越大,单个连接空转和数据库连接资源占用越多;真正的泄漏不修,多大池都会漏光。

References(参考来源)

最后更新于 2026-09-09

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法