数据库连接池被打满但 CPU 占用低怎么办?慢查询排查指南
问题现象
应用突然变慢或超时,监控显示:
- 应用日志:
Connection is not available, request timed out after 30000ms - 数据库连接池:所有连接被占满,等待队列堆积
- 数据库监控:CPU 占用率仅 20%,负载不高
这种"连接池干涸但 DB 不 busy"的不匹配现象,是典型的慢查询或锁竞争问题。
常见原因
| 原因 | 说明 | |------|------| | 索引失效 | 隐式类型转换、函数操作导致全表扫描 | | 锁竞争 | 长事务持有锁,阻塞其他查询 | | 大查询 | SELECT * 无 limit,返回百万级数据 | | 死锁 | 多个事务循环等待对方释放锁 | | 网络延迟 | 应用与数据库之间网络不稳定 |
排查步骤
步骤1:确认连接池状态
# 查看 HikariCP 连接池状态(如有 actuator)
curl http://localhost:8080/actuator/metrics/jdbc.connections.active
curl http://localhost:8080/actuator/metrics/jdbc.connections.pending
# 或查看应用日志中的连接池监控
步骤2:查看数据库当前连接和正在执行的 SQL
-- MySQL 查看当前连接
SHOW PROCESSLIST;
-- 或更详细的版本
SELECT
ID,
USER,
HOST,
DB,
COMMAND,
TIME,
STATE,
LEFT(INFO, 100) as SQL_PREVIEW
FROM INFORMATION_SCHEMA.PROCESSLIST
WHERE COMMAND != 'Sleep'
ORDER BY TIME DESC;
关键字段说明:
TIME:执行时间(秒),重点关注大于 10 秒的STATE:当前状态,如Sending data、Waiting for table lockINFO:正在执行的 SQL
步骤3:定位慢查询
-- 查看慢查询日志是否开启
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
-- 临时开启慢查询日志(当前会话)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过1秒记录
-- 直接查看执行中的慢查询
SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST
WHERE TIME > 10 AND COMMAND != 'Sleep';
步骤4:分析具体 SQL 的执行计划
-- 对可疑 SQL 执行 EXPLAIN
EXPLAIN SELECT * FROM orders
WHERE user_id = 123456
AND created_at > '2024-01-01';
-- 关注以下字段:
-- type: ALL(全表扫描) | index(索引扫描) | range(范围扫描) | ref(索引引用)
-- rows: 扫描行数,越大越慢
-- Extra: Using filesort(需优化) | Using temporary(需优化) | Using index(好)
步骤5:检查索引使用情况
-- 查看表索引
SHOW INDEX FROM orders;
-- 分析表统计信息
ANALYZE TABLE orders;
-- 查看索引选择性(cardinality 越高越好)
SHOW INDEX FROM orders WHERE Column_name = 'user_id';
步骤6:检查锁等待情况
-- MySQL 8.0+ 查看锁等待
SELECT
r.object_schema,
r.object_name,
r.thread_id AS waiting_thread,
r.processlist_info AS waiting_query,
b.thread_id AS blocking_thread,
b.processlist_info AS blocking_query
FROM performance_schema.data_lock_waits w
JOIN performance_schema.data_locks r ON w.requesting_lock_id = r.lock_id
JOIN performance_schema.data_locks b ON w.blocking_lock_id = b.lock_id;
-- MySQL 5.7 查看 InnoDB 锁状态
SHOW ENGINE INNODB STATUS;
-- 查看 LATEST DETECTED DEADLOCK 和 TRANSACTIONS 部分
常见原因及解决方法
原因1:隐式类型转换导致索引失效
-- 表结构
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id VARCHAR(32), -- 字符串类型
created_at DATETIME,
INDEX idx_user_id (user_id)
);
-- 错误写法:索引失效!
SELECT * FROM orders WHERE user_id = 123456; -- 数字类型,触发隐式转换
-- 正确写法:
SELECT * FROM orders WHERE user_id = '123456'; -- 字符串类型,走索引
-- 验证:
EXPLAIN SELECT * FROM orders WHERE user_id = 123456;
-- type: ALL (全表扫描)
EXPLAIN SELECT * FROM orders WHERE user_id = '123456';
-- type: ref (走索引)
原因2:对索引字段使用函数
-- 错误写法:索引失效
SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';
-- 正确写法:范围查询走索引
SELECT * FROM orders
WHERE created_at >= '2024-01-01 00:00:00'
AND created_at < '2024-01-02 00:00:00';
原因3:大查询无 limit
-- 错误写法:可能返回百万级数据
SELECT * FROM orders WHERE status = 'pending';
-- 正确写法:分页查询
SELECT * FROM orders
WHERE status = 'pending'
ORDER BY id
LIMIT 1000;
-- 或只取需要的字段
SELECT id, user_id, amount FROM orders WHERE status = 'pending';
原因4:长事务持有锁
-- 查看长事务
SELECT
trx_id,
trx_mysql_thread_id,
trx_state,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) as trx_seconds,
LEFT(trx_query, 100) as query_preview
FROM INFORMATION_SCHEMA.INNODB_TRX
ORDER BY trx_started;
-- 处理:
-- 1. 优化业务逻辑,缩短事务
-- 2. 将大事务拆分为小批次
-- 3. 非必要不放在事务中(如查询操作)
原因5:缺少合适的索引
-- 分析查询条件,创建复合索引
-- 原查询:
SELECT * FROM orders
WHERE user_id = '123'
AND status = 'paid'
AND created_at > '2024-01-01';
-- 创建复合索引(遵循最左前缀原则)
CREATE INDEX idx_user_status_created
ON orders(user_id, status, created_at);
-- 验证
EXPLAIN SELECT * FROM orders
WHERE user_id = '123'
AND status = 'paid'
AND created_at > '2024-01-01';
-- type: range, key: idx_user_status_created
预防措施
1. 配置慢查询监控
# my.cnf 配置
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.5 # 超过500ms记录
log_queries_not_using_indexes = 1 # 记录未使用索引的查询
2. 应用层配置查询超时
# Spring Boot 配置
spring:
datasource:
hikari:
connection-timeout: 30000 # 获取连接超时30秒
leak-detection-threshold: 60000 # 连接泄漏检测
jpa:
properties:
hibernate:
connection:
provider_disables_autocommit: true
3. 代码规范
// 1. 所有查询必须加 limit
@Query("SELECT o FROM Order o WHERE o.status = ?1 ORDER BY o.id LIMIT ?2")
List<Order> findByStatus(String status, int limit);
// 2. 大事务拆分
@Transactional
public void processOrders() {
// 错误:一次性处理所有
// List<Order> all = orderRepo.findAll();
// 正确:分批处理
Pageable pageable = PageRequest.of(0, 1000);
Page<Order> page;
do {
page = orderRepo.findByStatus("pending", pageable);
// 处理...
pageable = page.nextPageable();
} while (page.hasNext());
}
// 3. 查询超时设置
@QueryHints(@QueryHint(name = "javax.persistence.query.timeout", value = "5000"))
@Query("SELECT o FROM Order o WHERE ...")
List<Order> findWithTimeout();
验证修复
-- 1. 修复后重新执行 EXPLAIN,确认走了索引
EXPLAIN SELECT * FROM orders WHERE user_id = '123456';
-- 2. 查看慢查询日志是否还有该 SQL
-- tail -f /var/log/mysql/slow.log
-- 3. 监控连接池状态,确认连接数下降
-- 4. 压力测试验证
-- 使用 JMeter 或类似工具模拟并发
总结
| 症状 | 可能原因 | 快速检查 |
|------|---------|---------|
| 连接池满 + CPU 低 | 慢查询/锁等待 | SHOW PROCESSLIST |
| type=ALL | 索引失效 | EXPLAIN |
| STATE=Sending data | 大查询/全表扫描 | 检查 rows 字段 |
| Waiting for table lock | 锁竞争 | SHOW ENGINE INNODB STATUS |
一句话总结:连接池干涸但 DB CPU 低 = 查询被卡住,重点排查索引失效和长事务。
问题求助
没能解决你的问题?直接问我
如果你遇到任何技术问题无法解决,可以在这里提交求助。我会尽快查看并回复你。
支持作者
如果这篇文章帮到了你,可以支持我
扫码打赏,支持我持续更新原创排障文章。

