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日志,导致磁盘空间暴涨。
  • 优化策略:
    • 拆分事务: 将批量操作拆分为多个小事务,每批处理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 根因分析全景图

下图总结了五种慢查询根因的关联关系与排查优先级。箭头表示一个根因可能引发另一个根因(如大事务导致锁等待),虚线框表示该根因通常需要结合连接池与熔断策略共同治理。

慢查询根因分析全景图 慢查询 索引失效 数据倾斜 锁等待 大事务 SQL写法不当 大事务→锁等待 数据倾斜→锁等待 治理优先级: 1. 优先修复SQL写法不当与索引失效(低 hanging fruit,效果最明显) 2. 其次治理大事务与锁等待(需要业务侧配合,但收益大) 3. 最后处理数据倾斜(通常需要架构调整,周期较长) 连接池与熔断关联: • 锁等待 & 大事务 → 连接池连接被长时间占用 → 需调小 maxWait / 启用连接超时 • 数据倾斜 & 索引失效 → 单查询延迟高 → 需配置熔断阈值(慢查询比例 > 10% 时熔断)
实战建议: 在开始调优连接池参数之前,先对上述五个根因进行逐一排查。推荐使用慢查询日志(slow_query_log)配合性能监控工具(如Prometheus + Grafana),建立慢查询根因分类标签,为后续的熔断降级策略提供精准的触发条件。