一次 HikariCP 连接池耗尽导致的线上雪崩排查实录——问题概览 本文记录了一次线上接口大面积超时的完整排查过程从 MySQLLock 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;-- 查看当前连接数SHOWSTATUSLIKEThreads_connected;-- 查看最大连接数限制SHOWVARIABLESLIKEmax_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!SleepANDtime2ORDERBYtimeDESC;生产 MySQL 连接信息指标数值Threads_connected378max_connections4190连接使用率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是50mpc-server.yml是50patient.yml否默认 10portal.yml否默认 10operate.yml否默认 10baseservice.yml否默认 10…否默认 10核心高并发服务 patient/portal/operate 竟然都用的 HikariCP 默认 10 个连接而且hospital.yml里的connection-timeout配了 300000ms5 分钟这意味着一旦池子满了线程会阻塞 5 分钟才抛异常。Tomcat 线程很快就被占光服务对外表现为“全部接口超时”。3.2 量化一下10 个连接能抗多少并发我们以这次事故的“重灾区”syncOrderDeliveryInfo为例。这个接口在 V1 实现里会同步调用顺丰医寄通 API 下单整个过程平均要占住数据库连接约 20 秒**【这个物流问题之前说过优化过一个V2版本但为了降低风险仅开通一家医院调试其余80多家目前仍然用的V1】。**按 20 秒/请求估算单服务连接池大小最大并发数每分钟可处理请求1010~30 笔5050~150 笔100100~300 笔早高峰一个医院几笔、十几个医院同时发药30 笔/分钟根本不够看。连接池被打满是必然的。更扎心的是MySQLmax_connections有 4190全集群按 12 个服务 × 2 实例、多数 10 连接估算理论峰值才约 560 个连接只占 MySQL 容量的 13%。数据库能扛但应用层自己把并发能力锁死了。四、第三阶段深挖代码V1 大事务是放大器连接池小是直接原因但为什么连接会被占这么久得看代码。4.1 V1 入口TenantCallbackServiceImpl租户回调进来后会根据医院配置决定走 V1 还是 V2// TenantCallbackServiceImpl.javaOverridepublicSyncDeliveryResultVOsyncDeliveryInfo(SyncDeliveryDTOdto){StringhosCode...;IntegersyncDeliveryInfoVersionhospitalConfigApi.getSyncDeliveryInfoVersion(hosCode).getData();if(Integer.valueOf(2).equals(syncDeliveryInfoVersion)){returnorderApi.syncOrderDeliveryInfoV2(dto).getData();// V2}returnorderApi.syncOrderDeliveryInfo(dto).getData();// V1}当时只有一个医院开了 V2其他全部走 V1。4.2 V1 实现syncOrderDeliveryInfoByDeliveryV1 实现方法上直接加了Transactional// OrderServiceImpl.javaTransactional(rollbackForThrowable.class)publicSyncDeliveryResultVOsyncOrderDeliveryInfoByDelivery(LogisticsCompanyEnumlogisticsCompanyEnum,SyncDeliveryDTOdto){// ... 查询医院、租户配置、处方信息 ...// 事务内同步调用顺丰医寄通 API约 20sdeliveredInforpDeliveryService.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 大事务入口。维度V1V2入口流量绝大多数医院单个试点医院顺丰 API 调用位置Transactional事务内事务与锁之外事务范围大事务API DB 全包短事务仅 DB 写入连接占用时长数十秒数秒以内本次事故角色放大因子非主因五、根因定责我们把根因拆成两层直接原因配置层核心服务 HikariCPmaximum-pool-size仅 10单服务并发承载力极低早高峰迅速耗尽连接池是全服务雪崩的第一块多米诺骨牌。深层诱因/放大因子事务层V1syncOrderDeliveryInfoByDelivery在事务内同步调用顺丰 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里统一配置避免个别服务再用默认 10spring: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_connected387max_connections4190很容易说“数据库没问题”。但应用层每个服务只配了 10 个连接照样能把自己卡死。排查时要把应用连接池和数据库总连接数分开看。7.3 小连接池 长事务 雪崩加速器连接池只有20个实际需要50个 │ ▼ 连接全被占满新请求排队等连接 │ ▼ 等连接的请求越来越多越积越多 │ ▼ 等太久 → 超时 → 上游调用方也超时 │ ▼ 上游不断重试 → 压力更大 → 更多排队 │ ▼*雪崩*这次事故如果把连接池扩到 50或者把 V1 大事务拆成短事务任意一个改动都能避免雪崩。但两个坑同时踩就爆了。所以我们在做容量规划时不能只算 QPS还要算“每个请求平均占连接多久”。同时需要的连接数QPS× 平均每个请求占用连接的时间7.4 上线优化版本时要同步推进覆盖V2 已经是个正确实现但只开了一家医院。大多数流量还在 V1优化效果就出不来。这种“代码已优化、入口未切换”的情况在线上排查时很容易误判。八、结语这次事故最终的结论是应用层 HikariCP 连接池容量过小 V1 长事务放大连接占用共同导致的级联雪崩。连接池扩容是最快、最有效的止血手段而推进 V2、把外部 API 移出事务则是根治方向。希望对大家有参考价值。如果你们也遇到类似“数据库连接数不多但应用拿不到连接”的情况不妨先检查下 HikariCP 连接池配置很可能就是应用层自己把自己锁死了。相关文件完整排查报告一次 HikariCP 连接池耗尽导致的线上雪崩排查实录——完整排查报告