【连接池被我玩没了:一次Stream/Cursor没关导致Hikari枯竭的翻车记】
·
【今天下午正准备摸鱼,测试突然在群里@我:“下单列表一会儿就502,日志里还在报Hikari超时。”我心里一紧,看来不是普通慢SQL,是连接池被玩没了。)
事故现场
- 现象:
- 接口偶发30秒卡死,然后统一报错;
- 业务线程数不满,但DB连接池使用飙到顶;
- 网关/应用日志里不断出现连接获取超时。
日志随手一段:
2026-03-19 15:02:10 WARN com.zaxxer.hikari.pool.PoolBase - HikariPool-1 - Failed to validate connection com.mysql.cj.jdbc.ConnectionImpl@1f2c3a
2026-03-19 15:02:40 ERROR c.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Connection is not available, request timed out after 30000ms
org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection
把 leak 检测阈值开到 10s(线上临时):
-Dhikari.pool.HikariPool-1.leakDetectionThreshold=10000
马上刷出来:
HikariPool-1 - Connection leak detection triggered, stack trace follows
at com.xxx.order.dao.OrderDao.scanByDate(OrderDao.java:58)
at com.xxx.order.service.OrderService.listOrders(OrderService.java:143)
我当场裂开——十有八九是“流式查询/游标没关”,把连接占住不还,池子直接枯竭。
排查脑回路
- 先看DB:
SHOW PROCESSLIST;发现大量Sending data挂着不走,某些会话空闲但没释放。 - JFR/JStack看线程:一堆线程停在
Stream.filter/findFirst上,调用栈指向jdbcTemplate.queryForStream。 - 全局搜“queryForStream / Cursor”,两处可疑:
- Spring JdbcTemplate 的流式查询返回
Stream; - MyBatis 用了
Cursor<T>做大表扫描。
- Spring JdbcTemplate 的流式查询返回
- 打开代码,罪魁2连:
问题代码(反例)
1) queryForStream 没有关闭 Stream
public Optional<OrderDTO> findFirst(LocalDate day) {
Stream<OrderDTO> stream = jdbcTemplate.queryForStream(
"SELECT id,user_id,amount FROM t_order WHERE gmt_create >= ? AND gmt_create < ?",
ps -> {
ps.setTimestamp(1, Timestamp.valueOf(day.atStartOfDay()));
ps.setTimestamp(2, Timestamp.valueOf(day.plusDays(1).atStartOfDay()));
},
(rs, rowNum) -> map(rs)
);
// 这里直接返回/短路,没有显式 close()
return stream.filter(o -> o.getAmount().compareTo(BigDecimal.ZERO) > 0)
.findFirst();
}
queryForStream返回的是“懒读取”的Stream,不close就不归还连接;findFirst()提早返回时,后续 finally 也没机会清理,连接一直被占着。
2) MyBatis Cursor 用完不关
@Mapper
public interface OrderMapper {
@Select("SELECT id,user_id,amount FROM t_order WHERE status = 1")
@Options(fetchSize = Integer.MIN_VALUE)
Cursor<OrderDO> scanPaid();
}
public long sumPaid() {
Cursor<OrderDO> cursor = orderMapper.scanPaid();
long total = 0;
for (OrderDO o : cursor) { // 迭代完还好,异常/提前return 就泄了
total += o.getAmount().longValue();
if (total > 100000) return total; // 这里提前 return,cursor 没关
}
return total;
}
- MyBatis 的
Cursor本质也是流式结果 + 连接占用; - 提前 return/异常路径不 close,连接就被长期霸占。
根因
- 流式读取没配合 try-with-resources 正确关闭;
- 高峰期请求多,连接被长时间占用不归还,池子很快见底;
- 连接超时默认 30s,用户感知就是一刀 30s 的 504/500。
解决方案(一次性拔草)
1) 用 try-with-resources 兜住 queryForStream
public Optional<OrderDTO> findFirst(LocalDate day) {
try (Stream<OrderDTO> stream = jdbcTemplate.queryForStream(
"SELECT id,user_id,amount FROM t_order WHERE gmt_create >= ? AND gmt_create < ?",
ps -> {
ps.setTimestamp(1, Timestamp.valueOf(day.atStartOfDay()));
ps.setTimestamp(2, Timestamp.valueOf(day.plusDays(1).atStartOfDay()));
},
(rs, rowNum) -> map(rs)
)) {
return stream.filter(o -> o.getAmount().compareTo(BigDecimal.ZERO) > 0)
.findFirst();
}
}
或者干脆“物化”到内存再玩(数据量可控时):
List<OrderDTO> list = jdbcTemplate.query(sql, rowMapper, ...);
return list.stream().filter(...).findFirst();
2) MyBatis Cursor 必须包在 try-with-resources
public long sumPaid() {
try (Cursor<OrderDO> cursor = orderMapper.scanPaid()) {
long total = 0;
for (OrderDO o : cursor) {
total += o.getAmount().longValue();
if (total > 100000) return total; // try 会在 return 时自动 close
}
return total;
}
}
注意:
@Options(fetchSize = Integer.MIN_VALUE)只在流式场景生效(MySQL 驱动),非必须别乱加;- Cursor/Stream 别跨线程传递,读取线程与连接绑定。
3) Hikari 参数与监控
spring:
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
connection-timeout: 10000 # 快失败
validation-timeout: 3000
max-lifetime: 1700000 # < MySQL wait_timeout(1800s)
leak-detection-threshold: 10000 # 仅在排障阶段打开
maxLifetime要小于 MySQLwait_timeout,避免“借到快过期的连接”;- 排障时开
leakDetectionThreshold,定位堆栈;平时关掉以免有额外开销。
4) 兜底:统一工具封装,防止误用
public final class Streams {
public static <T,R> R withStream(Stream<T> s, Function<Stream<T>, R> fn) {
try (s) { return fn.apply(s); }
}
}
// 使用
return Streams.withStream(jdbcTemplate.queryForStream(sql, ps, rm),
st -> st.filter(...).findFirst().orElse(null));
验证
- 开始修复后,连接池 metrics:active 从 50/50 下降到 10~15,等待队列清零;
- 线上再无
Connection is not available报警,P99 从 31s 回到 120ms; SHOW PROCESSLIST无长时间Sending data残留。
踩坑总结
- 流式查询很香,但一定要配 try-with-resources;提前 return/异常也能正常 close;
- Cursor/Stream = 连接占用,别跨线程,别忘关;
- Hikari 连接池要有监控和快失败阈值,看到 leak 立刻定位,别让池子被榨干。】
更多推荐


所有评论(0)