報
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
調大池子只是緩衝,治標不治本。池子越大,單個連接空轉和數據庫連接資源佔用越多;真正的洩漏不修,多大池都會漏光。
