本文记录了一次线上接口大面积超时的完整排查过程:从 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连接信息:
sql
-- 查看当前所有连接
SHOW FULL PROCESSLIST;
-- 查看当前连接数
SHOW STATUS LIKE 'Threads_connected';
-- 查看最大连接数限制
SHOW VARIABLES LIKE 'max_connections';
-- 按用户/主机分组统计连接数
SELECT user, host, COUNT(*) AS conn_count
FROM information_schema.processlist
GROUP BY user, host;
-- 查看portal写入的表是否有锁等待
SELECT * FROM information_schema.innodb_trx ORDER BY trx_started ASC;
-- 查看是否有慢查询
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 2
ORDER BY time DESC;
生产 MySQL 连接信息:
| 指标 | 数值 |
|---|---|
Threads_connected |
378 |
max_connections |
4190 |
| 连接使用率 | 9.2% |

第一感觉:MySQL 没挂。 总连接数才用了不到 10%,不像是数据库扛不住了。
但奇怪的是,应用层却拿不到连接。那问题大概率出在应用层连接池。
2.2 再看应用日志:MyBatisSystemException
翻开 0727系统超时问题-怀疑是HikariPool连接池被打满了.txt,大量这种异常:
typescript
2026-07-27 09: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 笔/分钟根本不够看。连接池被打满是必然的。
更扎心的是,MySQL max_connections 有 4190,全集群按 12 个服务 × 2 实例、多数 10 连接估算,理论峰值才约 560 个连接,只占 MySQL 容量的 13%。数据库能扛,但应用层自己把并发能力锁死了。
四、第三阶段:深挖代码,V1 大事务是放大器
连接池小是直接原因,但为什么连接会被占这么久?得看代码。
4.1 V1 入口:TenantCallbackServiceImpl
租户回调进来后,会根据医院配置决定走 V1 还是 V2:
java
// TenantCallbackServiceImpl.java
@Override
public SyncDeliveryResultVO syncDeliveryInfo(SyncDeliveryDTO dto) {
String hosCode = ...;
Integer syncDeliveryInfoVersion = hospitalConfigApi.getSyncDeliveryInfoVersion(hosCode).getData();
if (Integer.valueOf(2).equals(syncDeliveryInfoVersion)) {
return orderApi.syncOrderDeliveryInfoV2(dto).getData(); // V2
}
return orderApi.syncOrderDeliveryInfo(dto).getData(); // V1
}
当时只有一个医院开了 V2,其他全部走 V1。
4.2 V1 实现:syncOrderDeliveryInfoByDelivery
V1 实现方法上直接加了 @Transactional:
java
// OrderServiceImpl.java
@Transactional(rollbackFor = Throwable.class)
public SyncDeliveryResultVO syncOrderDeliveryInfoByDelivery(LogisticsCompanyEnum logisticsCompanyEnum, SyncDeliveryDTO dto) {
// ... 查询医院、租户配置、处方信息 ...
// 事务内同步调用顺丰医寄通 API,约 20s
deliveredInfo = 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:
yaml
spring:
datasource:
hikari:
connection-init-sql: set names utf8mb4
minimum-idle: 10
maximum-pool-size: 50 # 核心服务统一 50,必要时单独覆盖 100
idle-timeout: 600000
max-lifetime: 1800000
connection-timeout: 5000 # 获取连接最多等待 5s,快速失败
auto-commit: true
leak-detection-threshold: 60000 # 连接泄漏检测 60s
同时把 MySQL innodb_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 小连接池 + 长事务 = 雪崩加速器
typescript
连接池只有 20 个,实际需要 50 个
│
▼
连接全被占满,新请求排队等连接
│
▼
等连接的请求越来越多(越积越多)
│
▼
等太久 → 超时 → 上游调用方也超时
│
▼
上游不断重试 → 压力更大 → 更多排队
│
▼
*雪崩*
这次事故如果把连接池扩到 50,或者把 V1 大事务拆成短事务,任意一个改动都能避免雪崩。但两个坑同时踩,就爆了。所以我们在做容量规划时,不能只算 QPS,还要算"每个请求平均占连接多久"。
typescript
同时需要的连接数 = QPS × 平均每个请求占用连接的时间
7.4 上线优化版本时要同步推进覆盖
V2 已经是个正确实现,但只开了一家医院。大多数流量还在 V1,优化效果就出不来。这种"代码已优化、入口未切换"的情况,在线上排查时很容易误判。
八、结语
这次事故最终的结论是:应用层 HikariCP 连接池容量过小 + V1 长事务放大连接占用,共同导致的级联雪崩。 连接池扩容是最快、最有效的止血手段;而推进 V2、把外部 API 移出事务,则是根治方向。
希望对大家有参考价值。如果你们也遇到类似"数据库连接数不多但应用拿不到连接"的情况,不妨先检查下 HikariCP 连接池配置,很可能就是应用层自己把自己锁死了。
相关文件: