线上接口突然大面积500,日志里刷的是同一句:
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms (total=10, active=10, idle=0, waiting=32)
字面意思是等30秒没等到空闲连接。根因可能是连接泄漏,可能是池配小了,也可能是数据库那边压根连不上。区分它们靠括号里那组数字。
括号里四个数字的读法
total、active、idle、waiting是抛异常那一刻的池快照。HikariCP 5.1.0、6.0.0、6.3.0、7.0.0,这几个版本都带这组数字;更早的HikariCP 2.7.9、3.4.5、4.0.3只报一句超时,没有快照,那种只能自己加监控。
total=0、active=0、idle=0、waiting=N:池里一条连接都没有。通常是启动阶段就没连上库,或者已有连接被全部判定失效剔除之后又补不回来。
total=10、active=10、idle=0、waiting=N:标准的池被打满。10条全借出去了,还有N个线程在排队,多半是连接泄漏或者慢查询占着不放。
total=10、active=1、idle=9、waiting=N,这种最坑人:明明有9条空闲却借不到。遇见过一次,是驱动的连接校验卡住了,每借一条都要先跑一次isValid,校验超时5秒,借一条卡一次,排队排到整体超时。日志里还会伴随一句Failed to validate connection。
确认是借不到还是连不上
翻启动日志,正常情况会打两行,一行是池开始填充、一行是填充完成,前缀都形如HikariPool-1。只有前一行没有后一行,说明池压根没起来,这时候报的超时跟业务量没关系。
池起不来时看initializationFailTimeout:默认值是1,启动阶段拿不到第一条连接就直接抛异常,应用起不来,属于快速失败;改成-1会跳过初始化继续启动,代价是第一次请求照样失败,等于把问题往后推。生产上倾向保留默认,起不来就起不来,别让一个带病的应用接流量。
连不上的原因里,驱动类名是个老坑。MySQL的com.mysql.jdbc.Driver早就废弃了,换成com.mysql.cj.jdbc.Driver;Spring Boot 2.0.0、2.7.18、3.2.0、3.4.0,这几个版本不配driver-class-name也能自动识别,配错了反而报Loading class的警告然后失败。
开泄漏检测,让HikariCP自己指出代码行
leakDetectionThreshold默认是0,也就是关闭。设一个比正常查询耗时长一些的值,借出去的连接超过这个时长没还,日志会打Apparent connection leak detected,后面直接跟借用处的堆栈:
spring.datasource.hikari.leak-detection-threshold=10000
这个值有下限,低于2000会被拒绝,实测设成2000在开发阶段最容易暴露问题,生产上给到10000到60000之间,避免慢查询刷一堆假警报。之前接手的一个项目,开了10000之后五分钟内就打出了三条泄漏堆栈,全指向一段手写JDBC的导出逻辑。
四种常见的连接没还
1、直接拿dataSource.getConnection() 用完之后没关。Spring的JdbcTemplate、JPA都会自动归还,手写的连接必须自己关,写法上用try-with-resources最稳,别指望finally里那句close一定被执行到。
2、JdbcTemplate的queryForStream返回的Stream没关。这个专门踩过:queryForStream的语义是流式读,连接在Stream关闭时才归还,中途return或者异常就漏掉了。改成queryForList一次性取回,或者显式包在try里。
3、自己createEntityManager之后不close。JPA的EntityManagerFactory手动建出来的实例不走Spring的事务管理,用完必须关。
4、事务里调外部接口。这个是占连接时间最长的:@Transactional标在方法上,连接从第一条SQL开始持有,方法里再调一个5秒才返回的支付网关或者短信接口,这条连接就白占5秒。现场见过一次这种写法,把外部调用挪到事务外面,池压力立刻降下来。
池大小不是越大越好
maximumPoolSize默认值是10,minimumIdle默认等于maximumPoolSize。官方给的估算口径是核心数乘2再加上磁盘有效轴数,一台4核挂SSD的库算出来是9,取10就够。往上堆到几百条,数据库那边光是上下文切换和锁争用就把吞吐吃回去了。
算池大小的时候别忘了乘实例数。5个实例各配30,数据库要能扛150条并发连接,而MySQL的max_connections默认只有151,一台应用集群就能把它打满,那时候报的就不是HikariCP的超时,而是数据库侧的Too many connections。
几个参数之间的牵制
connectionTimeout默认30000,最小允许250,设成比250小的值会被系统属性挡回去(4.0.3起留了开关可以放行)。这个值定的是调用方的耐心,接口SLA是3秒的话给30000毫无意义,请求早就超时了,不如给3000让问题早点暴露。
validationTimeout默认5000,它必须小于connectionTimeout。
maxLifetime默认1800000,也就是30分钟,必须设得比数据库的wait_timeout短。MySQL的wait_timeout默认是28800秒(8小时),30分钟远小于它,安全;反过来如果库那边设成了600秒而maxLifetime还是30分钟,就会不断拿到已被服务端关掉的连接,日志里刷Communications link failure。另外这个值带随机衰减,6.1.0起衰减幅度从2.5%调到了25%,就是为了让连接不要在同一秒集体到期。
keepaliveTime默认是0,也就是不保活。HikariCP 6.2.1起默认值改成了2分钟,这个值要求大于30000且小于maxLifetime,用来定期ping一下空闲连接,能挡掉中间防火墙或者负载均衡器悄悄掐掉的空闲连接。
把池状态接到监控上
Spring Boot加了actuator就能直接看,重点盯pending这一项:
curl -s localhost:8080/actuator/metrics/hikaricp.connections.pending curl -s localhost:8080/actuator/metrics/hikaricp.connections.active curl -s localhost:8080/actuator/metrics/hikaricp.connections.timeout
pending持续大于0就是池有压力了,比等到报超时再处理从容得多。没接actuator的,JMX里HikariPool的MBean也有同样的指标。
已经出事的时候,抓一份线程栈看有多少线程卡在getConnection上,比看日志快:
jstack -l <pid> > /tmp/stack.txt grep -c "HikariPool.getConnection" /tmp/stack.txt
数据库侧同步看一眼
连得上但借不到,多数是应用侧的事;连不上,就得看数据库。MySQL上跑show processlist看当前连接数与状态,Sleep状态特别多说明应用在占着不干活,配合information_schema里按用户分组统计,能定位是哪个实例在耗连接。
还有一个容易被忽略的时间点:2018年Spring Boot 2.0起默认连接池从Tomcat JDBC换成了HikariCP,很多老项目升级之后沿用原来的spring.datasource.tomcat.* 配置,那些参数全部失效、全部走默认,池大小一下从50掉到10,上线就报这个错。升级之后记得把配置项前缀一起改成spring.datasource.hikari。
数据截至2026年10月,HikariCP版本与默认值以官方文档为准。