MySQL全面瓦解21(番外):一次深夜优化亿级数据分页的奇妙经历

MySQL全面瓦解21(番外):一次深夜优化亿级数据分页的奇妙经历,第1张

概述背景 1月22号晚上10点半,下班后愉快的坐在在回家的地铁上,心里想着周末的生活怎么安排。 突然电话响了起来,一看是我们的一个开发同学,顿时紧张了起来,本周的版本已经发布过了,这时候打电话一般来说是线 背景

1月22号晚上10点半,下班后愉快的坐在在回家的地铁上,心里想着周末的生活怎么安排。

突然电话响了起来,一看是我们的一个开发同学,顿时紧张了起来,本周的版本已经发布过了,这时候打电话一般来说是线上出问题了。

果然,沟通的情况是线上的一个查询数据的接口被疯狂的失去理智般的调用,这个 *** 作直接导致线上的MysqL集群被拖慢了。

好吧,这问题算是严重了,下了地铁匆匆赶到家,开电脑,跟同事把Pinpoint上的慢查询日志捞出来。看到一个很奇怪的查询,如下

1 POST  domain/v1.0/module/method?order=condition&orderType=desc&offset=1800000&limit=500

domainmodulemethod 都是化名,代表接口的模块实例方法名后面的offset和limit代表分页 *** 作的偏移量和每页的数量,也就是说该同学是在 翻第(1800000/500+1=3601)页。初步捞了一下日志,发现 有8000多次这样调用。

这太神奇了,而且我们页面上的分页单页数量也不是500,而是 25条每页,这个绝对不是人为的在功能页面上进行一页一页的翻页 *** 作,而是数据被刷了(说明下,我们生产环境数据有1亿+) 详细对比日志发现,很多分页的时间是重叠的,对方应该是多线程调用。

通过对鉴权的Token的分析,基本定位了请求是来自一个叫做APIautotest的客户端程序在做这个 *** 作,也定位了生成鉴权Token的账号来自一个QA的同学。立马打电话给同学,进行了沟通和处理。

分析

其实对于我们的MySQL查询语句来说,整体效率还是可以的,该有的联表查询优化都有,该简略的查询内容也有,关键条件字段和排序字段该有的索引也都在,问题在于他一页一页的分页去查询,查到越后面的页数,扫描到的数据越多,也就越慢。

我们在查看前几页的时候,发现速度非常快,比如  limit 200,25,瞬间就出来了。但是越往后,速度就越慢,特别是百万条之后,卡到不行,那这个是什么原理呢。先看一下我们翻页翻到后面时,查询的sql是怎样的:

1 select * from t_name where c_name1='xxx' order by c_name2 limit 2000000,25;

这种查询的慢,其实是因为limit后面的偏移量太大导致的。比如像上面的  limit 2000000,25 ,这个等同于数据库要扫描出 2000025条数据,然后再丢弃前面的 20000000条数据,返回剩下25条数据给用户,这种取法明显不合理。

 

 

大家翻看《高性能MysqL》第六章:查询性能优化,对这个问题有过说明: 

分页 *** 作通常会使用limit加上偏移量的办法实现,同时再加上合适的order by子句。但这会出现一个常见问题:当偏移量非常大的时候,它会导致MysqL扫描大量不需要的行然后再抛弃掉。 数据模拟

那好,了解了问题的原理,那就要试着解决它了。涉及数据敏感性,我们这边模拟一下这种情况,构造一些数据来做测试。

1、创建两个表:员工表和部门表

 1 /*部门表,存在则进行删除 */ 2 drop table if EXISTS dep; 3 create table dep( 4     ID int unsigned primary key auto_increment, 5     depno mediumint unsigned not null default 0,1)"> 6     depname varchar(20) default "",1)"> 7     memo 200)  "" 8 ); 9 10 员工表,存在则进行删除11  emp;12  emp(13     ID 14     empno mediumint unsigned 15     empname 16     job 9) 17     mgr mediumint unsigned 18     hiredate datetime null19     sal decimal(7,1); Font-weight: bold">2) 20     comn 21     depno mediumint unsigned 022 );

2、创建两个函数:生成随机字符串和随机编号

 产生随机字符串的函数DEliMITER $ FUNCTION  rand_string; 4 CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255) 5 BEGIN 6     DECLARE chars_str 100) DEFAulT 'abcdefghijklmlopqrstuvwxyzABCDEFGHIJKLMnopQRSTUVWXYZ'; 7     DECLARE return_str 255) '' 8     DECLARE i INT DEFAulT  9     WHILE i < n DO10     SET return_str = CONCAT(return_str,SUBSTRING(chars_str,1)">FLOOR(1+RAND()*52),1); Font-weight: bold">1));11     SET i = i+12     END WHILE13     RETURN return_str;14 END $15 DEliMITER;16 17 18 产生随机部门编号的函数19 20  rand_num;21 FUNCTION rand_num() INT(522 23     24     = 1001025      i;26 27 DEliMITER;

3、编写存储过程,模拟500W的员工数据

建立存储过程:往emp表中插入数据DEliMITER $PROCEDURE  insert_emp;PROCEDURE insert_emp(IN START 10),1)">IN max_num ))set autocommit =0 把autocommit设置成0,把默认提交关闭SET autocommit =  9     REPEAT= i + INSERT INTO emp(empno,empname,job,mgr,hiredate,sal,comn,depno) VALUES ((START+i),rand_string(6),1)">SALEMAN',1); Font-weight: bold">0001,Now(),1); Font-weight: bold">2000,1); Font-weight: bold">40012     UNTIL i = max_num REPEAT;14     COMMIT16 17 插入500W条数据18 call insert_emp(0,1); Font-weight: bold">5000000);

4、编写存储过程,模拟120的部门数据

建立存储过程:往dep表中插入数据 insert_dept;PROCEDURE insert_dept(INSERT  INTO dep( depno,depname,memo) VALUES((START811     UNTIL i 插入120条数据17 call insert_dept(1,1); Font-weight: bold">120);

 5、建立关键字段的索引,这边是跑完数据之后再建索引,会导致建索引耗时长,但是跑数据就会快一些。

建立关键字段的索引:排序、条件2 INDEX IDx_emp_ID ON emp(ID);3 INDEX IDx_emp_depno  emp(depno);4 INDEX IDx_dep_depno ON dep(depno); 
测试测试数据
偏移量为100,取25SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depnamefrom emp a left join dep b on a.depno = b.depno order by a.ID desc limit 100,1); Font-weight: bold">25偏移量为4800000,取255 6 4800000,1); Font-weight: bold">25; 
执行结果
 1 [sql] 4 受影响的行:  5 时间: .001s 6  7  9 受影响的行: 10 时间: 12.275s
因为扫描的数据多,所以这个明显不是一个量级上的耗时。 解决方案1、使用索引覆盖+子查询优化因为我们有主键ID,并且在上面建了索引,所以可以先在索引树中找到开始位置的 ID值,再根据找到的ID值查询行数据。
子查询获取偏移100条的位置的ID,在这个位置上往后取25 b.depnowhere a.ID >= (select ID from emp by ID limit by a.ID limit  6 子查询获取偏移4800000条的位置的ID,在这个位置上往后取2525;

执行结果

执行效率相比之前有大幅的提升:
 6 受影响的行:  7 时间: .106s 8   9 13 14 受影响的行: 15 时间: 1.541s  
2、起始位置重定义记住上次查找结果的主键位置,避免使用偏移量 offset
记住了上次的分页的最后一条数据的ID是100,这边就直接跳过100,从101开始扫描表 a.ID,a.empno,1)">where a.ID > 100  5   6 记住了上次的分页的最后一条数据的ID是4800000,这边就直接跳过4800000,从4800001开始扫描表where a.ID > 480000025;

执行结果

> 100  5 受影响的行:  6 时间:  7   8 480000013 受影响的行: 14 时间: 0.000s 
这个效率是最好的,无论怎么分页,耗时基本都是一致的,因为他执行完条件之后,都只扫描了25条数据。但是有个问题,只适合一页一页的分页,这样才能记住前一个分页的最后ID。如果用户跳着分页就有问题了,比如刚刚刷完第25页,马上跳到35页,数据就会不对。这种的适合场景是类似百度搜索或者腾讯新闻那种滚轮往下拉,不断拉取不断加载的情况。这种延迟加载会保证数据不会跳跃着获取。3、降级策略看了网上一个阿里的dba同学分享的方案:配置limit的偏移量和获取数一个最大值,超过这个最大值,就返回空数据。因为他觉得超过这个值你已经不是在分页了,而是在刷数据了,如果确认要找数据,应该输入合适条件来缩小范围,而不是一页一页分页。这个跟我同事的想法大致一样:request的时候 如果offset大于某个数值就先返回一个4xx的错误。 小结:当晚我们应用上述第三个方案,对offset做一下限流,超过某个值,就返回空值。第二天使用第一种和第二种配合使用的方案对程序和数据库脚本进一步做了优化。合理来说做任何功能都应该考虑极端情况,设计容量都应该涵盖极端边界测试。另外,该有的限流、降级也应该考虑进去。比如工具多线程调用,在短时间频率内8000次调用,可以使用计数服务判断并反馈用户调用过于频繁,直接给予断掉。哎,大意了啊,搞了半夜,QA同学不讲武德。不过这是很美好的经历了。

总结

以上是内存溢出为你收集整理的MySQL全面瓦解21(番外):一次深夜优化亿级数据分页的奇妙经历全部内容,希望文章能够帮你解决MySQL全面瓦解21(番外):一次深夜优化亿级数据分页的奇妙经历所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/sjk/1151528.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-31
下一篇 2022-05-31

发表评论

登录后才能评论

评论列表(0条)

保存