报
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。这种故障有传染性:一个接口泄漏连接,量一大,别的纯读接口也会因为拿不到连接而被拖垮,最终整站雪崩。
最常见的泄漏来源是两类:
- 连接借了不还:手写
getConnection()却不用 try-with-resources 关闭。 @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
调大池子只是缓冲,治标不治本。池子越大,单个连接空转和数据库连接资源占用越多;真正的泄漏不修,多大池都会漏光。
