一次 HikariCP 连接池耗尽导致的线上雪崩排查实录——问题概览
本文记录了一次线上接口大面积超时的完整排查过程:从 MySQL
Lock wait timeout异常,到怀疑连接池被打满,再到代码层发现 V1 大事务在事务内同步调用外部顺丰 API,最终定位到应用层 HikariCP 连接池容量过小是直接诱因。希望对遇到类似问题的同学有所帮助。
一、事故背景
我们做的是一套互联网医院系统,核心模块包括处方、订单、物流、HIS 对接等。日常早高峰(8:30-10:00)是租户侧发药、HIS 处方同步最密集的时段。
2026-07-27 09:05 左右,监控告警开始狂响:大量接口响应超时,租户回调失败,患者端发药、订单查询频繁报错。故障持续到 09:50 左右才逐渐恢复。
现象速览
- 大部分接口返回“网络繁忙,请稍候重试”或“系统异常”;
- 租户侧的
syncDeliveryInfo(发药/物流同步)接口大量失败; - 非核心接口(如保存系统交互日志、预问诊信息查询)也报错,说明不是单个业务挂了;
- MySQL 日志出现大量
Lock wait timeout exceeded; - 应用日志出现大量
org.mybatis.spring.MyBatisSystemException: null。
第一直觉:这不是单纯的代码 bug,而是某个公共资源被耗尽了。
二、第一阶段:定位是数据库问题,还是连接池问题?
2.1 先看 MySQL 连接数
执行sql,查询mysql连接信息:
-- 查看当前所有连接SHOWFULLPROCESSLIST;-- 查看当前连接数SHOWSTATUSLIKE'Threads_connected';-- 查看最大连接数限制SHOWVARIABLESLIKE'max_connections';-- 按用户/主机分组统计连接数SELECTuser,host,COUNT(*)ASconn_countFROMinformation_schema.processlistGROUPBYuser,host;-- 查看portal写入的表是否有锁等待SELECT*FROMinformation_schema.innodb_trxORDERBYtrx_startedASC;-- 查看是否有慢查询SELECTid,user,host,db,command,time,state,infoFROMinformation_schema.processlistWHEREcommand!='Sleep'ANDtime>2ORDERBYtimeDESC;生产 MySQL 连接信息:
| 指标 | 数值 |
|---|---|
Threads_connected | 378 |
max_connections | 4190 |
| 连接使用率 | 9.2% |
第一感觉:MySQL 没挂。总连接数才用了不到 10%,不像是数据库扛不住了。
但奇怪的是,应用层却拿不到连接。那问题大概率出在应用层连接池。
2.2 再看应用日志:MyBatisSystemException
翻开0727系统超时问题-怀疑是HikariPool连接池被打满了.txt,大量这种异常:
2026-07-2709:23:51.909[portal,...][http-nio-8881-exec-11]-[WARN][c.c.m.i.s.impl.SystemInteractionLogServiceImpl:211]-错误参数:org.mybatis.spring.MyBatisSystemException at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(...)at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:441)...注意这个异常 message 是null,底层真正的Connection is not available, request timed out after 300000ms被全局异常处理吞掉了。这给我的排查增加了不少难度。最終还是找到了连接池耗尽的关键证据!!
到这里,基本可以判断:HikariCP 连接池已经耗尽,新请求拿不到连接。
三、第二阶段:为什么连接池会满?
3.1 检查 Nacos 配置,发现核心服务连接池只有 10
我们的数据源配置统一放在 Nacos 的common-mysql.yml里,但每个服务可以单独覆盖。逐个检查后发现:
| 配置文件 | 是否显式配置 maximum-pool-size | 连接池大小 |
|---|---|---|
hospital.yml | 是 | 50 |
mpc-server.yml | 是 | 50 |
patient.yml | 否 | 默认 10 |
portal.yml | 否 | 默认 10 |
operate.yml | 否 | 默认 10 |
baseservice.yml | 否 | 默认 10 |
| … | 否 | 默认 10 |
核心高并发服务 patient/portal/operate 竟然都用的 HikariCP 默认 10 个连接!
而且hospital.yml里的connection-timeout配了 300000ms(5 分钟),这意味着一旦池子满了,线程会阻塞 5 分钟才抛异常。Tomcat 线程很快就被占光,服务对外表现为“全部接口超时”。
3.2 量化一下:10 个连接能抗多少并发?
我们以这次事故的“重灾区”syncOrderDeliveryInfo为例。这个接口在 V1 实现里会同步调用顺丰医寄通 API 下单,整个过程平均要占住数据库连接约 20 秒**【这个物流问题之前说过,优化过一个V2版本,但为了降低风险,仅开通一家医院调试,其余80多家目前仍然用的V1】。**
按 20 秒/请求估算:
| 单服务连接池大小 | 最大并发数 | 每分钟可处理请求 |
|---|---|---|
| 10 | 10 | ~30 笔 |
| 50 | 50 | ~150 笔 |
| 100 | 100 | ~300 笔 |
早高峰一个医院几笔、十几个医院同时发药,30 笔/分钟根本不够看。连接池被打满是必然的。
更扎心的是,MySQLmax_connections有 4190,全集群按 12 个服务 × 2 实例、多数 10 连接估算,理论峰值才约 560 个连接,只占 MySQL 容量的 13%。数据库能扛,但应用层自己把并发能力锁死了。
四、第三阶段:深挖代码,V1 大事务是放大器
连接池小是直接原因,但为什么连接会被占这么久?得看代码。
4.1 V1 入口:TenantCallbackServiceImpl
租户回调进来后,会根据医院配置决定走 V1 还是 V2:
// TenantCallbackServiceImpl.java@OverridepublicSyncDeliveryResultVOsyncDeliveryInfo(SyncDeliveryDTOdto){StringhosCode=...;IntegersyncDeliveryInfoVersion=hospitalConfigApi.getSyncDeliveryInfoVersion(hosCode).getData();if(Integer.valueOf(2).equals(syncDeliveryInfoVersion)){returnorderApi.syncOrderDeliveryInfoV2(dto).getData();// V2}returnorderApi.syncOrderDeliveryInfo(dto).getData();// V1}当时只有一个医院开了 V2,其他全部走 V1。
4.2 V1 实现:syncOrderDeliveryInfoByDelivery
V1 实现方法上直接加了@Transactional:
// OrderServiceImpl.java@Transactional(rollbackFor=Throwable.class)publicSyncDeliveryResultVOsyncOrderDeliveryInfoByDelivery(LogisticsCompanyEnumlogisticsCompanyEnum,SyncDeliveryDTOdto){// ... 查询医院、租户配置、处方信息 ...// 事务内同步调用顺丰医寄通 API,约 20sdeliveredInfo=rpDeliveryService.mode(rpDeliveryMode).handleDelivered(logisticsCompanyEnum,rpDeliveryData);// 事务内更新处方状态orderPrescriptionService.lambdaUpdate()...;// 事务内更新订单状态this.lambdaUpdate()...;// 事务内更新 HIS 订单状态hisOrderService.lambdaUpdate()...;}问题很明显:顺丰 API 调用和后面的 DB 更新都在同一个事务里。顺丰 API 和部分业务处理,耗时约 20 秒,这 20 秒内数据库连接一直被占用,同时ihm_order_prescription等表的行锁也被持有。
4.3 为什么是 V1 而不是 V2 的锅?
有同事一开始怀疑是新上的 V2 接口导致的超时。我们核对后发现:
- V2 仅对一家医院开放,早晨只有几笔订单;
- V2 虽然也要同步调用顺丰 API,但已经把 API 调用放在事务和 Redisson 锁之外了;
- V2 只用
TransactionTemplate包裹处方/订单/HIS 的 DB 更新,锁也只覆盖 DB 写入段。
所以 V2 本身不是元凶,反而是正确的优化方向。真正的风险是:大多数医院还在走 V1 大事务入口。
| 维度 | V1 | V2 |
|---|---|---|
| 入口流量 | 绝大多数医院 | 单个试点医院 |
| 顺丰 API 调用位置 | @Transactional事务内 | 事务与锁之外 |
| 事务范围 | 大事务,API + DB 全包 | 短事务,仅 DB 写入 |
| 连接占用时长 | 数十秒 | 数秒以内 |
| 本次事故角色 | 放大因子 | 非主因 |
五、根因定责
我们把根因拆成两层:
- 直接原因(配置层):核心服务 HikariCP
maximum-pool-size仅 10,单服务并发承载力极低,早高峰迅速耗尽连接池,是全服务雪崩的第一块多米诺骨牌。 - 深层诱因/放大因子(事务层):V1
syncOrderDeliveryInfoByDelivery在事务内同步调用顺丰 API和部分业务处理 约 20 秒,使单连接长时间被占用并持有ihm_order_prescription行锁,让小连接池更快耗尽。
也就是说,即便 V1 大事务存在,只要连接池配得足够大,系统也能扛过当前流量;而小连接池让这个问题在早高峰被指数级放大。
六、解决方案与落地
6.1 紧急止血:先扩连接池
事发时最优先的操作:把 patient、portal、operate 等核心服务的maximum-pool-size临时调到 50~100,并把connection-timeout降到 3000~5000ms。
这是见效最快的手段。按 20 秒/请求估算,扩到 50 后单服务并发能力从 30 req/min 提升到 150 req/min,能立刻利用 MySQL 剩余的连接容量。
6.2 短期:统一 Nacos 配置
在common-mysql.yml里统一配置,避免个别服务再用默认 10:
spring:datasource:hikari:connection-init-sql:set names utf8mb4minimum-idle:10maximum-pool-size:50# 核心服务统一 50,必要时单独覆盖 100idle-timeout:600000max-lifetime:1800000connection-timeout:5000# 获取连接最多等待 5s,快速失败auto-commit:trueleak-detection-threshold:60000# 连接泄漏检测 60s同时把 MySQLinnodb_lock_wait_timeout从 50 秒降到 10~20 秒,让锁竞争快速失败,避免线程长期占用连接。
6.3 中期:推进 V2 覆盖或改造 V1 事务边界
顺丰 API 不能异步(HIS 要实时拿到物流单号),但可以把调用移出事务:
- 推进 V2:逐步把更多医院的
sync_delivery_info_version设为 2,从入口上消除 V1 大事务。 - 过渡改造 V1:如果部分医院短期无法切 V2,可以把
rpDeliveryService.handleDelivered(...)提前到事务开启前执行,或者用TransactionTemplate只包裹 DB 写入段。
6.4 长期:监控与规范
- 接入 HikariCP Micrometer metrics,监控
activeConnections、pendingThreads; - 全局异常处理必须打印完整 cause,别再让
MyBatisSystemException: null这种日志出现; - 制定规范:核心服务禁止使用默认连接池配置,外部 API 禁止放在事务内。
七、复盘与踩坑经验
7.1 不要被异常 message 骗了
这次日志里MyBatisSystemException: null让我们绕了不少弯路。如果全局异常处理能把底层Connection is not available, request timed out after 300000ms打出来,问题定位能快很多。异常处理里千万别吞 cause。
7.2 MySQL 连接数充足 ≠ 应用没问题
DBA 一看Threads_connected=387,max_connections=4190,很容易说“数据库没问题”。但应用层每个服务只配了 10 个连接,照样能把自己卡死。排查时要把应用连接池和数据库总连接数分开看。
7.3 小连接池 + 长事务 = 雪崩加速器
连接池只有20个,实际需要50个 │ ▼ 连接全被占满,新请求排队等连接 │ ▼ 等连接的请求越来越多(越积越多) │ ▼ 等太久 → 超时 → 上游调用方也超时 │ ▼ 上游不断重试 → 压力更大 → 更多排队 │ ▼*雪崩*这次事故如果把连接池扩到 50,或者把 V1 大事务拆成短事务,任意一个改动都能避免雪崩。但两个坑同时踩,就爆了。所以我们在做容量规划时,不能只算 QPS,还要算“每个请求平均占连接多久”。
同时需要的连接数=QPS× 平均每个请求占用连接的时间7.4 上线优化版本时要同步推进覆盖
V2 已经是个正确实现,但只开了一家医院。大多数流量还在 V1,优化效果就出不来。这种“代码已优化、入口未切换”的情况,在线上排查时很容易误判。
八、结语
这次事故最终的结论是:应用层 HikariCP 连接池容量过小 + V1 长事务放大连接占用,共同导致的级联雪崩。连接池扩容是最快、最有效的止血手段;而推进 V2、把外部 API 移出事务,则是根治方向。
希望对大家有参考价值。如果你们也遇到类似“数据库连接数不多但应用拿不到连接”的情况,不妨先检查下 HikariCP 连接池配置,很可能就是应用层自己把自己锁死了。
相关文件:
- 完整排查报告:一次 HikariCP 连接池耗尽导致的线上雪崩排查实录——完整排查报告