一次线上慢SQL与接口超时问题排查
问题现象
这是一次典型的后台列表接口超时。
接口除了查主表,还会补下游状态和展示字段。高峰期一上来就开始抖:
1、列表接口平时大约 200ms ~ 300ms
2、高峰期偶发升到 3s ~ 5s
3、数据库连接占用上升,但应用 CPU 并不高
4、网关有超时报警,应用日志里经常看到分页查询耗时偏长
这类问题不能只盯一层。表面是接口超时,实际往往是分页 SQL、事务边界和补充查询叠在一起。
第一轮排查
先看应用日志
先补分段耗时,结果很快就出来了:主要时间都耗在列表查询。
1 | long start = System.currentTimeMillis(); |
到这一步已经能先排掉 JSON 序列化和返回组装,问题在数据库查询。
再看慢查询
再从慢查询日志里捞 SQL,马上能看到典型的 order by + limit offset 深分页。
1 | SELECT id, biz_id, group_id, status, update_time |
数据量小时这类 SQL 看不出问题,数据一上来,深分页代价就全出来了。
看执行计划
1 | EXPLAIN SELECT id, biz_id, group_id, status, update_time |
执行计划重点看三项:
1、扫描行数很大
2、Using where
3、Using filesort
这说明过滤条件虽然生效了,但排序和分页没有命中合适索引,数据库还在扫和排大量记录。
根因分析
最后定位下来,不是单一 SQL 问题,是两层问题叠在一起。
1、索引不合适
原来的索引偏单列或通用索引,但这条 SQL 的过滤和排序是组合出现的:biz_id + status + update_time。
单列索引帮不上太多,最后还是要回表、排序、扫大量数据。
2、分页过深
LIMIT 5000, 20 这种深分页,本质上前面 5000 条也得先扫一遍,页数越深越慢。
后台管理列表最容易出现这种情况,前几页正常,翻深了就开始抖。
第二轮排查
只改索引后,RT 下来了,但高峰期还是偶发超时。继续看监控,发现事务持有时间偏长。
顺着调用链往下追,发现原代码把“查列表 + 补状态 + 补展示信息”全包在一个大事务里。
1 |
|
问题很直接:
1、数据库事务时间被无谓拉长
2、补充状态和展示信息一旦慢,就会放大接口整体 RT
3、并发高时更容易把连接和锁占住
解决方案
1、补组合索引
按查询条件和排序方式补组合索引,不要继续堆单列索引。
1 | ALTER TABLE t_activity_entry |
这里把 id 也带上,是为了让排序和分页路径更稳定。
2、改深分页
能不用深分页,就不要一直 LIMIT offset, size。
更稳的方式是基于上一页最后一条记录继续翻页,比如按 update_time + id 做游标翻页。
1 | SELECT id, biz_id, group_id, status, update_time |
3、缩短事务范围
事务里只保留必要的数据库操作,补充查询和结果组装放到事务外。
1 | public PageResult<ActivityEntryVO> queryPage(QueryReq req) { |
其他补充查询也放到事务外,不要把事务时间拖长。
结果
改完以后,结果直接看这几项:
1、慢查询数量明显下降
2、接口 TP99 从秒级回落到几百毫秒
3、数据库连接占用更平稳
4、超时报警基本消失
这类问题以后怎么避免
这不是一次性的失误,是列表查询里很容易重复出现的问题。
以后排这类问题,顺序就按这个来:
1、先分清是本地 SQL 慢,还是补充查询慢
2、慢查询先看执行计划,不要凭感觉猜索引
3、分页接口重点看是否存在 order by + limit offset 的深分页
4、事务里不要包下游调用和复杂组装逻辑
5、改完以后一定压测,不要只看本地跑通
小结
线上超时很多时候不是某个服务突然不行了,而是分页查询、索引设计、事务边界和补充调用叠出来的。
排这类问题,不要只盯一层,要把接口、SQL、事务边界和补充调用串起来看。



