2. 慢查询根因分析:索引失效、数据倾斜、锁等待、大事务、SQL写法不当
慢查询的根因通常集中在五个核心领域:索引失效、数据倾斜、锁等待、大事务和SQL写法不当。本节将逐一剖析每个根因的触发机制、诊断方法和典型场景,帮助你在调优连接池和熔断策略前,先精准定位慢查询的源头。
诊断原则: 遇到慢查询时,优先检查索引与SQL写法(占比约70%),其次排查锁与事务(占比约20%),最后确认数据分布是否倾斜。不要盲目调整连接池参数。
2.1 索引失效
索引失效是慢查询最常见的原因。即使表上建有索引,错误的SQL写法或数据变化也会导致优化器放弃索引扫描,转而执行全表扫描。
| 失效场景 | 典型SQL示例 | 失效原因 | 修复建议 |
|---|---|---|---|
| 隐式类型转换 | WHERE phone = 13800138000(phone为varchar) |
数据库将字段转为数字,导致索引失效 | 使用字符串常量:WHERE phone = '13800138000' |
| 函数运算 | WHERE DATE(create_time) = '2024-01-01' |
对索引列使用函数,优化器无法使用索引 | 改为范围查询:WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02' |
| 前导模糊匹配 | WHERE name LIKE '%张三%' |
左模糊导致B+树无法定位起始位置 | 改为右模糊:WHERE name LIKE '张三%',或使用全文索引 |
| 联合索引未用最左前缀 | 索引(a,b,c),查询WHERE b=1 AND c=2 |
跳过第一列,索引无法匹配 | 调整查询条件顺序,或为高频查询重建索引 |
| OR条件不当 | WHERE a=1 OR b=2(a有索引,b无索引) |
优化器评估全表扫描比索引+回表更优 | 为b也建立索引,或改为UNION ALL |
诊断命令: 使用
EXPLAIN 查看 type 字段。如果显示 ALL(全表扫描)或 index(全索引扫描),且 rows 远大于预期,则大概率存在索引失效。重点关注 Extra 列中的 Using where; Using index(索引覆盖)与 Using filesort(文件排序,通常需要优化)。
2.2 数据倾斜
数据倾斜指数据在分区或分片节点上分布严重不均,导致少数节点成为性能瓶颈。在分布式数据库或分库分表场景下尤为突出。
- 表现: 整体CPU/IO不高,但个别节点负载打满;查询延迟波动大,部分请求超时。
- 常见原因:
- 分片键选择不当: 使用性别、状态码等低基数字段作为分片键,导致大量数据落入同一分片。
- 热点数据: 某条记录(如大V用户、爆款商品)被高频访问,单节点压力过大。
- JOIN倾斜: 大表JOIN小表时,关联键存在大量重复值,导致Reduce阶段数据倾斜。
- 诊断方法:
- 查看各分片的数据行数或磁盘占用:
SELECT shard_key, COUNT(*) FROM table GROUP BY shard_key ORDER BY COUNT(*) DESC。 - 监控各节点活跃连接数和查询响应时间,识别热点节点。
- 查看各分片的数据行数或磁盘占用:
- 解决思路:
- 重新设计分片键,使用高基数且访问均匀的字段(如用户ID、订单ID)。
- 对热点数据增加二级缓存(Redis),减少数据库穿透。
- JOIN倾斜时,将小表广播到所有节点,或对倾斜key进行加盐打散。
注意: 数据倾斜导致的慢查询,单纯调整连接池参数(如增大maxActive)可能适得其反——更多请求涌入热点节点,导致该节点连接耗尽甚至宕机。必须先治理倾斜,再考虑连接池优化。
2.3 锁等待
锁等待是指一个事务持有锁,另一个事务等待该锁释放,导致查询阻塞。锁等待时间过长会直接表现为慢查询,且通常伴随应用层连接池连接被长时间占用。
| 锁类型 | 触发场景 | 诊断SQL(MySQL) | 影响 |
|---|---|---|---|
| 行锁(Record Lock) | 并发更新同一行记录 | SHOW ENGINE INNODB STATUS\G 查看 LATEST DETECTED DEADLOCK |
单行更新串行化,TPS下降 |
| 间隙锁(Gap Lock) | 可重复读隔离级别下,范围查询未命中索引 | SELECT * FROM performance_schema.data_locks\G |
锁范围扩大,阻塞相邻记录插入 |
| 表锁(MDL) | DDL操作(ALTER TABLE)与DML并发 | SHOW PROCESSLIST 查看 Waiting for table metadata lock |
全表阻塞,连接池连接迅速耗尽 |
| 死锁 | 两个事务互相等待对方持有的锁 | 应用日志报 Deadlock found when trying to get lock |
事务回滚,应用重试,增加响应延迟 |
快速定位: 在MySQL中执行
SELECT * FROM sys.innodb_lock_waits\G,可以直接看到当前阻塞的事务ID、等待时间、持有的锁和等待的锁。等待时间超过 innodb_lock_wait_timeout(默认50秒)时,事务会回滚并报错。
2.4 大事务
大事务指执行时间过长或操作数据量过大的事务。大事务不仅自身执行慢,还会引发锁扩散、undo日志膨胀、主从延迟等一系列连锁问题。
- 典型特征:
- 事务内包含大量UPDATE/DELETE操作(如一次删除10万行)。
- 事务内包含远程RPC调用或业务逻辑等待(如循环调用外部API)。
- 事务长时间不提交,持有锁不释放。
- 诊断方法:
- MySQL:
SELECT * FROM information_schema.innodb_trx WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 10查看运行超过10秒的事务。 - 查看undo表空间大小:大事务会产生大量undo日志,导致磁盘空间暴涨。
- MySQL:
- 优化策略:
- 拆分事务: 将批量操作拆分为多个小事务,每批处理1000~5000行,并提交。
- 避免事务内RPC: 将远程调用移到事务外部,或使用异步消息队列。
- 设置超时: 应用层设置事务超时时间(如@Transactional(timeout=5)),防止事务无限等待。
大事务与连接池: 大事务长时间占用数据库连接,导致连接池中的连接被“饿死”。即使连接池总大小充足,可用连接数也会迅速归零。此时应优先治理大事务,而非单纯增大连接池。
2.5 SQL写法不当
SQL写法不当是开发阶段最容易引入的问题,通常不涉及索引缺失,而是查询逻辑本身存在性能缺陷。
| 不当写法 | 问题说明 | 优化方案 |
|---|---|---|
| SELECT * | 返回所有列,增加IO和网络开销,无法使用覆盖索引 | 只查询需要的列 |
| N+1查询 | 循环中逐条查询数据库,产生大量小查询 | 使用JOIN或IN子查询合并为一次查询 |
| 子查询嵌套过深 | MySQL对派生表优化不佳,产生临时表 | 改写为JOIN或使用CTE(公用表表达式) |
| ORDER BY + LIMIT 偏移量大 | LIMIT 100000, 20 需要扫描并排序前100020行 |
使用游标分页(WHERE id > last_id) |
| 未使用索引排序 | ORDER BY字段无索引,触发filesort | 为排序字段建立索引,或调整查询顺序 |
| COUNT(DISTINCT) 大表 | 需要扫描全表去重计数,性能极差 | 使用近似计数(如Redis HyperLogLog)或维护计数表 |
-- 反例:N+1查询
for (User user : userList) {
Order order = orderMapper.selectByUserId(user.getId()); // 循环内查询
}
-- 正例:批量查询
List<Long> userIds = userList.stream().map(User::getId).collect(Collectors.toList());
List<Order> orders = orderMapper.selectByUserIds(userIds); // 一次查询
2.6 根因分析全景图
下图总结了五种慢查询根因的关联关系与排查优先级。箭头表示一个根因可能引发另一个根因(如大事务导致锁等待),虚线框表示该根因通常需要结合连接池与熔断策略共同治理。
实战建议: 在开始调优连接池参数之前,先对上述五个根因进行逐一排查。推荐使用慢查询日志(slow_query_log)配合性能监控工具(如Prometheus + Grafana),建立慢查询根因分类标签,为后续的熔断降级策略提供精准的触发条件。