这次事故也是我们组里遇到的一次关于分页慢查询的典型例子,通过这篇文章,你可以很清晰的跟随我们还原事故现场,以及每一步遇到问题做出的调整和改动。
打开机器监控,发现⼏乎所有机器的TP999都异常的⾼,观察机器CPU监控,发现CPU使⽤率并不⾼
定位到这里,我们基本确定这个不是几分钟能解决的问题,于是我们分成两步去处理。第一步:打开限流,防止更多的慢sql请求进行 第二步:分析慢sql,进行改造上线 查看慢SQL,⼤部分都是融合系统分⻚查询接⼝涉及到的SQL,同时由于上游系统在15:35左右对于该接⼝调⽤流量激增,和数据库CPU暴涨,接⼝TP999暴涨的时间吻合,推测是由于库存对于该接⼝的调⽤对于数据库造成了压⼒,导致接⼝耗时增加。但是该接⼝的调⽤量并不⾼,再次查看慢SQL,发现有⼤量已经遍历到⼏百⻚的慢SQL。推测是深分⻚的问题。
问题SQL:
select * from table where org_code = xxxx limit 1000,100
以上⾯的SQL为例,MySQL的limit⼯作原理就是先读取前⾯1000条记录,然后抛弃前1000条,读后⾯100条想要的,所以⻚码越⼤,偏移量越⼤,性能就越差。
即先使⽤查询条件查询出来id,再通过id进⾏范围查询,也就是说我第⼀次优化的时候使⽤的⽅法 ⾸先查询出来ID,以上⾯的SQL为例
select id from table where org_code = xxxx limit 1000,5
然后查询出来id后,使⽤id进⾏in查询,由于是直接基于主键的in查询,所以效率较⾼
select * from table where id in (1,2,3,4,5);
由于在第⼀次查询已经查询出来了所有符合条件的ID了,可以使⽤范围查询来替代in查询,效率更⾼(in 查询需要和集合⾥⾯的元素进⾏⽐对,但是范围查询只需要⽐较最⼤和最⼩即可)
select * from table where org_code = xxxx and id >= 1 and id <= 5;
使⽤⼦查询
select a.id,a.dj_sku_id,a.jd_sku_id from table a join (select id from
jd_spu_sku where org_code = xxxx limit 1000,5) b
on a.id = b.id;
使⽤⼦查询可以减少和数据库的IO交互,也是⼀种常⽤的解决深分⻚的⽅法。
每次接⼝都会返回查询出来的数据的最⼤的id(游标),下⼀次查询传⼊这个游标,服务端只需要根据这个游标,取出id⼤于这个游标的n个数据即可。n为每⻚展示条数。
select * from table where org_code = xxxx and id > 0 limit 10;
这种⽅式服务端实现起来⽐较简单且性能很好。缺点是需要客户端修改,且需要保证ID是⾃增有序且结果需要是按照ID排序的。最终定下的是使⽤滚动查询的⽅法。最终优化SQL上线后,表现平稳。第⼆周和库存⼀起重新优化了⾮多规格SKU的SQL。如下:
SELECT id,dj_org_code,dj_sku_id,jd_sku_id,yn FROM table where
org_code = xxxx and id > 0 order by id asc limit 500
测试了没问题后上线。观察线上监控稳定。本以为⾼枕⽆忧的时候,⼀周之后,数据库再次出现了⼤量的慢查询,数据库CPU报警,观察接⼝监控:
可以看到在调⽤量并不⼤的前提下,接⼝的耗时达到了60S。联系运维同学帮忙排查,发现了⼤量的慢 SQL:
SELECT id,dj_org_code,dj_sku_id,jd_sku_id,yn FROM table where
org_code = xxxx and id > 0 order by id asc limit 500
可以看出来,这就是我们优化后的SQL。运维同学explain这条sql后发现,这条SQL⾛了主键索引,没有⾛我们以为应该要⾛的org_code的索引。
和运维初步沟通后得出结论,在某些情况下,主键索引的优先级是会⾼于普通索引的。
因为我们使⽤了主键索引进⾏排序,且查询了不在索引树只在叶⼦节点中的字段。因此mysql认为主键索引更优,因为既可以排序,⼜不⽤回表,所以就使⽤主键索引最终导致了全表扫描。
最终使⽤了先查询ID(不查询叶⼦节点字段保证使⽤索引),在通过join,使⽤查询出来的ID来查询对应的数据的SQL:
select a.id AS id,a.dj_org_code AS djOrgCode,a.dj_sku_id AS
djSkuId,a.jd_sku_id AS jdSkuId,a.yn AS yn from
table a join
(
SELECT id FROM table where org_code = xxxx and id > 0 order
by id asc limit 500
) t on a.id=t.id;
再次explain了下,可以发现⾛了我们既定的索引:
于是上线,解决问题。上线稳定后,分析之前的问题SQL,执⾏下⾯两条语句,同样的SQL,不同的商家,MYSQL的执⾏结果也是不⼀样的
查阅资料得知
SELECT id,dj_org_code,dj_sku_id,jd_sku_id,yn FROM table force
index(idx_upc) where org_code = xxxx and id > 0 order by id asc limit
500
但是这种写死了索引名称的⽅式,如果以后修改了索引名,容易导致安全隐患。
全部0条评论
快来发表一下你的评论吧 !