2. 数据库连接池耗尽:一个慢查询引发的连锁反应
我记得刚带团队那会儿,遇到过一件特诡异的事。一个接口平时响应也就 30ms,突然某天开始隔三差五超时。重启就好了,但过几个小时又不行了。查监控发现,数据库连接池被占满了。
最开始我以为是并发太高,加了一倍连接数。结果呢?崩得更快了。
慢查询怎么把池子弄爆的
你想想看,一个正常查询 10ms 就释放连接了。但一个慢查询跑了 5 秒。如果这个慢查询的 QPS 是 100,那在 5 秒内,它同时占着 500 个连接不还。而你的连接池总共才 200 个——直接炸穿。
这就是典型的水位堰塞效应:
- 健康查询:借连接 → 10ms 归还 → 连接周转快
- 慢查询:借连接 → 5秒后归还 → 连接周转慢 500 倍
- 等待队列:新请求进来,等不到空闲连接 → 超时 → 客户端重试 → 更挤
我调过最夸张的一次,一个没加索引的联表查询跑了 12 秒。就这一个查询,干翻了一整个支付服务。
连锁反应的全过程
光连接池爆掉还不是最要命的。它会引发一串雪崩——我称之为「五连击」:
- 第一击:慢查询占满活跃连接 —— 业务 SQL 没走索引,全表扫描
- 第二击:健康请求排队饿死 —— 连心跳检测的 SQL 都拿不到连接
- 第三击:健康检查失败 —— 注册中心觉得服务挂了,摘掉节点
- 第四击:流量倾斜到剩余节点 —— 剩下一台机器扛双倍流量,也挂了
- 第五击:集群雪崩 —— 整个服务集体扑街
这个链条里,最隐蔽的是第三步。我见过很多团队,连接池监控报警没配,只看接口超时率。结果从慢查询到服务下线,中间隔了快 20 分钟才被用户发现。
我怎么定位这个问题的
定位这种问题,核心就一句话:查等待,不查来源。什么意思?不要直接去翻代码找哪个接口慢,而是去数据库里看「连接在等什么」。
我一般三步走:
第一步:查活跃连接数
-- MySQL
SHOW PROCESSLIST;
-- 看 Time 列,超过 1 秒的都是可疑对象
-- 重点关注 State = 'Sending data' 或 'Creating sort index'
第二步:查锁等待
SELECT * FROM information_schema.INNODB_TRX;
SELECT * FROM sys.innodb_lock_waits;
-- 有没有长事务?有没有行锁没释放?
第三步:查慢查询日志
-- 开启慢查询日志(生产环境建议长期开,阈值设 200ms)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.2;
-- 然后用 pt-query-digest 分析
pt-query-digest /var/log/mysql/slow.log
有一回我查到一个连接睡了 45 秒,SQL 是个 SELECT * FROM orders WHERE status = 1。一张 2000 万行的表,status 字段没索引。就这么简单。
避坑指南:我曾经...
犯过什么错呢? 我曾经一上来就把连接池从 100 加到 500,觉得「管你慢不慢,池子大就不怕」。结果数据库连接数暴增,CPU 被上下文切换干满了,DB 直接 OOM。
正确的解决方案长什么样
我的做法分三层:
| 层级 | 工具/手段 | 目标 |
|---|---|---|
| 应用层 | 连接池参数调优 + 断连检测 | 快速回收「假死连接」 |
| 数据库层 | 慢查询日志 + 索引优化 | 消灭慢 SQL 源头 |
| 监控层 | 连接池水位告警 + 慢 SQL 实时上报 | 在雪崩前截断 |
举个具体配置的例子。HikariCP 是我的首选连接池,参数调好了真能救命:
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 20 # 别贪大,够用就行
minimum-idle: 5 # 预留保底连接
connection-timeout: 3000 # 3秒拿不到连接就报错
idle-timeout: 600000 # 空闲 10 分钟回收
max-lifetime: 1800000 # 连接最多活 30 分钟
leak-detection-threshold: 5000 # 连接借出去 5 秒没还,报警!
我还想让连接池更安全一点
除了参数调优,我建议所有接口都做 连接超时兜底。也就是在业务代码里,给数据库操作加一层熔断:
// 伪代码:连接池熔断示例
CompletableFuture<Result> future = CompletableFuture.supplyAsync(() -> {
return orderService.queryOrders(userId);
});
try {
// 整个查询最多等 2 秒
return future.get(2000, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
// 超时了,先记录慢查询日志,再返回降级结果
monitor.reportSlowQuery("queryOrders", userId);
return fallbackResult;
}
这样做,就算某个查询真的变慢了,它也不会无限占着连接不还。2 秒一到,连接回收,服务继续跑。
复盘:这个案例教会我的
回头再看这个坑,其实有几个关键点被忽略了:
- 慢查询不能只看「执行时间」,要看「占用连接的时间」。一个 200ms 的查询,在连接池里可能排队等了 5s,实际占用连接也是 5s。
- 连接池水位告警必须比接口超时告警更敏感。接口超时是症状,连接池水位是病因。
- 不要只堆机器。不加索引,加十台机器也顶不住。