news 2026/9/29 8:14:47

连接池又双叒枯竭了:Hikari + Stream/Cursor 未关闭的排查与配置复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
连接池又双叒枯竭了:Hikari + Stream/Cursor 未关闭的排查与配置复盘

1. 连接池又双叒枯竭了:从一次 queryForStream 泄漏说起

Hikari 连接池枯竭是 Java 后端最常见的线上事故之一,而queryForStream返回的 Stream 忘记 close、MyBatis 的 Cursor 提前 return 没关闭,是其中最隐蔽的一类。它不像慢 SQL 那样一眼能看出来,业务线程数不满、CPU 不高,但数据库连接池 active 直接顶到maximumPoolSize,接口统一卡 30 秒然后报Connection is not available。这篇就聚焦这个场景:Hikari 连接池被 Stream/Cursor 占住不还,怎么用leakDetectionThreshold定位、怎么用 try-with-resources 修复、怎么用连接池指标验证泄漏真的消除了。适合正在用 Spring JdbcTemplate 流式查询、MyBatis Cursor 做大表扫描的 Java 后端同学,尤其是遇到过「偶发 502、日志刷 Hikari 超时」但没找到根因的人。

先说结论:queryForStream返回的 Stream 是懒读取的,它背后绑着一个真实的 JDBC Connection,不 close 就不归还;findFirst()、anyMatch()这类短路操作会提前结束,但不会自动释放连接。MyBatis 的Cursor<T>同理,本质是流式结果集加连接占用,异常路径或提前 return 都会泄漏。下面按「现象 → 定位 → 修复 → 验证」走一遍,每一步都能直接复制。

2. 前置准备:TaoToken 与本地环境

在动手排查之前,先把模型对话和接入文档的入口准备好,方便边查 API 边对照参数。我平时用 TaoToken 做接口联调和文档查询,它的模型对话入口可以直接问 Hikari 参数含义,接入文档里有完整的 Key 申请和调用示例。

  • 模型对话(问参数、问报错):https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite
  • 接入文档(看 API 规范):https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
  • API Keys 管理(生成调用凭证):https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite
  • 控制台(看用量和状态):https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
  • API 基址(代码里配置):https://taotoken.net/api

本地环境需要:JDK 8+、Spring Boot 2.x/3.x、HikariCP(Spring Boot 默认自带)、MySQL 5.7/8.0。确认spring-boot-starter-jdbc或mybatis-spring-boot-starter已引入,Hikari 是默认连接池,不需要额外加依赖。如果你用的是 Druid 或 Tomcat JDBC,参数名不同,但泄漏原理一致。

注意:leakDetectionThreshold只在排障阶段打开,它会给每个连接加一个定时检查,生产长期开启有额外开销。定位到堆栈后立刻关掉。

3. 可复制配置:Hikari 关键参数与修复骨架

3.1 Hikari 参数怎么配才不容易被榨干

先给一份可以直接抄的application.yml,重点看maximum-pool-size、connection-timeout、max-lifetime、leak-detection-threshold四个。

spring: datasource: url: jdbc:mysql://127.0.0.1:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai username: app_user password: your_password hikari: pool-name: HikariPool-Order maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 10000 validation-timeout: 3000 idle-timeout: 600000 max-lifetime: 1700000 leak-detection-threshold: 10000

几个参数的关系要理清:connection-timeout是拿不到连接时的等待上限,默认 30 秒,用户感知就是卡 30 秒然后 500,建议压到 10 秒快失败;max-lifetime必须小于 MySQL 的wait_timeout(默认 1800 秒),否则会借到已经被服务端断开的连接,报Failed to validate connection;leak-detection-threshold设成 10000 表示连接借出超过 10 秒没还就打印堆栈,这是定位泄漏的核心开关。

3.2 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) ); return stream.filter(o -> o.getAmount().compareTo(BigDecimal.ZERO) > 0) .findFirst(); }

正确写法用 try-with-resources 兜住 Stream,无论正常返回、提前 return 还是抛异常,close 都会执行。

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(); } }

如果数据量可控(几千行以内),更省心的做法是直接物化到内存,用普通query返回 List,再对 List 做 stream 操作,连接在方法返回前就已经归还。

List<OrderDTO> list = jdbcTemplate.query(sql, rowMapper, day.atStartOfDay(), day.plusDays(1).atStartOfDay()); return list.stream().filter(o -> o.getAmount().compareTo(BigDecimal.ZERO) > 0).findFirst();

3.3 MyBatis Cursor 的修复骨架

Cursor 同样必须包在 try-with-resources 里,提前 return 时 try 块会自动 close。

@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() { try (Cursor<OrderDO> cursor = orderMapper.scanPaid()) { long total = 0; for (OrderDO o : cursor) { total += o.getAmount().longValue(); if (total > 100000) { return total; } } return total; } }

@Options(fetchSize = Integer.MIN_VALUE)是 MySQL 驱动流式读取的开关,只在真正需要流式时加,普通查询别乱加,否则每次都是一行一行取,反而更慢。Cursor 和 Stream 都不能跨线程传递,读取线程和连接是绑定的,丢到线程池里异步消费必然出问题。

3.4 统一封装,防止团队误用

光靠 code review 挡不住,最好封一个工具方法,让调用方没法忘记 close。

public final class Streams { private 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(o -> o.getAmount().compareTo(BigDecimal.ZERO) > 0).findFirst().orElse(null) );

4. 验证请求:日志与连接池指标怎么确认泄漏消除

4.1 用 leakDetectionThreshold 抓堆栈

线上临时开启泄漏检测,可以通过启动参数或配置中心动态下发。

java -Dhikari.pool.HikariPool-Order.leakDetectionThreshold=10000 -jar order-service.jar

触发后日志会打印类似这样的堆栈,直接指向没关的代码行。

HikariPool-Order - 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)

看到堆栈后,对照第 3 节的骨架改代码,改完再观察日志里还有没有新的 leak 记录。

4.2 看连接池指标

Spring Boot Actuator 暴露 Hikari 指标后,可以直接查 active 和 pending。

curl -s http://127.0.0.1:8080/actuator/metrics/hikaricp.connections.active curl -s http://127.0.0.1:8080/actuator/metrics/hikaricp.connections.pending

修复前 active 长期贴着 50/50,pending 排队;修复后 active 回落到 10~15,pending 归零。这是最直观的验证信号。

4.3 看数据库侧会话

SHOW PROCESSLIST;

修复前会看到大量Sending data状态挂着不走,或者空闲但没释放的会话;修复后这些长时间残留消失。配合SHOW STATUS LIKE 'Threads_connected'看连接数是否稳定。

4.4 压测复现与回归

用 JMeter 或 wrk 对下单列表接口打 200 并发持续 5 分钟,观察 P99 和错误率。修复前 P99 会飙到 30 秒以上并出现 500;修复后 P99 回到百毫秒级,无Connection is not available报错。这一步做完,基本可以确认泄漏被拔掉了。

5. 本篇常见错排查

报错一:Connection is not available, request timed out after 30000ms这是池子被占满的典型表现,不是网络问题。先开leakDetectionThreshold抓堆栈,重点搜queryForStream、Cursor、Stream关键字。如果堆栈指向业务代码里的流式查询,按第 3 节改。

报错二:Failed to validate connection通常是max-lifetime大于 MySQLwait_timeout,借到了服务端已断开的连接。把max-lifetime调到小于wait_timeout(比如 1700000 毫秒对 1800 秒),并确认validation-timeout合理。

报错三:改了 try-with-resources 但 active 还是高检查是不是有 Stream 被返回到了方法外,或者 Cursor 被丢进了线程池异步消费。Stream/Cursor 的生命周期必须和连接绑定在同一个线程、同一个方法内。另外确认没有在@Transactional里做流式查询后长时间持有,事务不提交连接也不还。

报错四:leakDetectionThreshold开了但没日志确认 pool-name 和参数前缀对得上,-Dhikari.pool.HikariPool-Order.leakDetectionThreshold里的 pool-name 要和配置里一致。用配置中心动态刷新时,注意 Hikari 部分参数不支持热更新,需要重启或重建数据源。

报错五:MyBatis Cursor 迭代到一半抛异常try-with-resources 会在异常路径自动 close,但如果你手动cursor.close()又在外层 catch 里再操作 cursor,会报已关闭。统一用 try-with-resources,不要在 finally 里重复 close。

6. 长期编码与 Agent 场景的接入建议

如果你在做的是长期编码、Agent 工具链或者需要频繁调用模型做代码审查,单次对话入口不够用,建议走 Coding Plan,把模型能力接进日常开发流。

  • Coding Plan(长期编码/Agent 场景):https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite
  • Claude Code 接入说明:https://taotoken.net/claude-code?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite

回到连接池这件事,最后补一个我踩过的坑:queryForStream配合@Transactional时,即使 Stream 关了,如果事务没提交,连接也不会立刻归还。排查时别只盯着 Stream,把事务边界一起看。另外maximum-pool-size不是越大越好,50 已经能扛住大多数业务,盲目调到 200 只会把数据库连接数打满,问题从应用层转移到 DB 层。先把泄漏堵住,再谈扩容。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 8:14:03

godot + vscode ai开发:用 TaoToken 统一 Key 打通编辑器补全与调试配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 8:03:54

软硬协同,同星EOL下线测试方案重塑产线终检新体验

EOL&#xff08;End of Line&#xff09;测试是汽车零部件生产制造过程中&#xff0c;产品完成所有装配工艺后的最后一道关键质检环节。在严苛的生产节拍下&#xff0c;测试系统需模拟真实车载环境&#xff0c;对ECU进行全方位的电气、通讯及逻辑功能验证&#xff0c;保障每一台…

作者头像 李华
网站建设 2026/9/29 8:03:02

Paperclip协议:AI Agent三端通信的轻量级HTTP协议栈

1. “Paperclip”不是回形针&#xff1a;它是一套面向AI原生应用的轻量级协议栈最近在几个技术社区里频繁看到“paperclip”这个词&#xff0c;尤其和Node.js、React、OpenClaw、Claude这些词高频共现。一开始我也以为是某个UI组件库——毕竟React生态里叫“clip”“paper”“c…

作者头像 李华
网站建设 2026/9/29 8:01:27

Hermes Agent 安装失败自救:9 个阶段、3 份日志、5 步恢复

Hermes Agent 安装失败自救&#xff1a;9 个阶段、3 份日志、5 步恢复 【免费下载链接】hermes-agent The agent that grows with you 项目地址: https://gitcode.com/GitHub_Trending/he/hermes-agent Hermes Agent 是 Nous Research 出品的自进化 AI 智能体——从执行…

作者头像 李华