点击关注下方公众号,架构师全套资料 都在这里

0、2T架构师学习资料干货分享

上一篇:危险!请马上替换代码中的BeanUtils!!!

大家好,我是互联网架构师。

作者:翁智华
出处:https://www.cnblogs.com/wzh2010/

背景

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

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

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

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

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

domain、module 和 method 都是化名,代表接口的域、模块和实例方法名,后面的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,6     depname varchar(20) not null default "",7     memo varchar(200) not null default ""8 );9
10 /*员工表,存在则进行删除*/
11 drop table if EXISTS emp;
12 create table emp(
13     id int unsigned primary key auto_increment,
14     empno mediumint unsigned not null default 0,
15     empname varchar(20) not null default "",
16     job varchar(9) not null default "",
17     mgr mediumint unsigned not null default 0,
18     hiredate datetime not null,
19     sal decimal(7,2) not null,
20     comn decimal(7,2) not null,
21     depno mediumint unsigned not null default 0
22 );

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

1 /* 产生随机字符串的函数*/2 DELIMITER $ 3 drop FUNCTION if EXISTS rand_string;4 CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)5 BEGIN6     DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmlopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';7     DECLARE return_str VARCHAR(255) DEFAULT '';8     DECLARE i INT DEFAULT 0;9     WHILE i < n DO
10     SET return_str = CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
11     SET i = i+1;
12     END WHILE;
13     RETURN return_str;
14 END $
15 DELIMITER;
16
17
18 /*产生随机部门编号的函数*/
19 DELIMITER $
20 drop FUNCTION if EXISTS rand_num;
21 CREATE FUNCTION rand_num() RETURNS INT(5)
22 BEGIN
23     DECLARE i INT DEFAULT 0;
24     SET i = FLOOR(100+RAND()*10);
25     RETURN i;
26 END $
27 DELIMITER;

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

1 /*建立存储过程:往emp表中插入数据*/2 DELIMITER $3 drop PROCEDURE if EXISTS insert_emp;4 CREATE PROCEDURE insert_emp(IN START INT(10),IN max_num INT(10))5 BEGIN6     DECLARE i INT DEFAULT 0;7     /*set autocommit =0 把autocommit设置成0,把默认提交关闭*/8     SET autocommit = 0;9     REPEAT
10     SET i = i + 1;
11     INSERT INTO emp(empno,empname,job,mgr,hiredate,sal,comn,depno) VALUES ((START+i),rand_string(6),'SALEMAN',0001,now(),2000,400,rand_num());
12     UNTIL i = max_num
13     END REPEAT;
14     COMMIT;
15 END $
16 DELIMITER;
17 /*插入500W条数据*/
18 call insert_emp(0,5000000);

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

1 /*建立存储过程:往dep表中插入数据*/2 DELIMITER $3 drop PROCEDURE if EXISTS insert_dept;4 CREATE PROCEDURE insert_dept(IN START INT(10),IN max_num INT(10))5 BEGIN6     DECLARE i INT DEFAULT 0;7     SET autocommit = 0;8     REPEAT9     SET i = i+1;
10     INSERT  INTO dep( depno,depname,memo) VALUES((START+i),rand_string(10),rand_string(8));
11     UNTIL i = max_num
12     END REPEAT;
13     COMMIT;
14 END $
15 DELIMITER;
16 /*插入120条数据*/
17 call insert_dept(1,120);

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

1 /*建立关键字段的索引:排序、条件*/
2 CREATE INDEX idx_emp_id ON emp(id);
3 CREATE INDEX idx_emp_depno ON emp(depno);
4 CREATE INDEX idx_dep_depno ON dep(depno);

测试

测试数据

1 /*偏移量为100,取25*/
2 SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname
3 from emp a left join dep b on a.depno = b.depno order by a.id desc limit 100,25;
4 /*偏移量为4800000,取25*/
5 SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname
6 from emp a left join dep b on a.depno = b.depno order by a.id desc limit 4800000,25;

执行结果

1 [SQL]2 SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname3 from emp a left join dep b on a.depno = b.depno order by a.id desc limit 100,25;4 受影响的行: 05 时间: 0.001s6 [SQL]7 SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname8 from emp a left join dep b on a.depno = b.depno order by a.id desc limit 4800000,25;9 受影响的行: 0
10 时间: 12.275s

因为扫描的数据多,所以这个明显不是一个量级上的耗时。另外,MySQL 系列面试题和答案全部整理好了,微信搜索互联网架构师,在后台发送:面试,可以在线阅读。

解决方案

1、使用索引覆盖+子查询优化

因为我们有主键id,并且在上面建了索引,所以可以先在索引树中找到开始位置的 id值,再根据找到的id值查询行数据。

1 /*子查询获取偏移100条的位置的id,在这个位置上往后取25*/2 SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname3 from emp a left join dep b on a.depno = b.depno4 where a.id >= (select id from emp order by id limit 100,1)5 order by a.id limit 25;6 7 /*子查询获取偏移4800000条的位置的id,在这个位置上往后取25*/8 SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname9 from emp a left join dep b on a.depno = b.depno
10 where a.id >= (select id from emp order by id limit 4800000,1)
11 order by a.id limit 25;

执行结果

执行效率相比之前有大幅的提升:

1 [SQL]2 SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname3 from emp a left join dep b on a.depno = b.depno4 where a.id >= (select id from emp order by id limit 100,1)5 order by a.id limit 25;6 受影响的行: 07 时间: 0.106s8  9 [SQL]
10 SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname
11 from emp a left join dep b on a.depno = b.depno
12 where a.id >= (select id from emp order by id limit 4800000,1)
13 order by a.id limit 25;
14 受影响的行: 0
15 时间: 1.541s

2、起始位置重定义

记住上次查找结果的主键位置,避免使用偏移量 offset

1 /*记住了上次的分页的最后一条数据的id是100,这边就直接跳过100,从101开始扫描表*/2 SELECT a.id,a.empno,a.empname,a.job,a.sal,b.depno,b.depname3 from emp a left join dep b on a.depno = b.depno4 where a.id > 100 order by a.id limit 25;5  6 /*记住了上次的分页的最后一条数据的id是4800000,这边就直接跳过4800000,从4800001开始扫描表*/7 SELECT a.id,a.empno,a.empname,a.job,a.sal,b.depno,b.depname8 from emp a left join dep b on a.depno = b.depno9 where a.id > 4800000
10 order by a.id limit 25;

执行结果

1 [SQL]2 SELECT a.id,a.empno,a.empname,a.job,a.sal,b.depno,b.depname3 from emp a left join dep b on a.depno = b.depno4 where a.id > 100 order by a.id limit 25;5 受影响的行: 06 时间: 0.001s7  8 [SQL]9 SELECT a.id,a.empno,a.empname,a.job,a.sal,b.depno,b.depname
10 from emp a left join dep b on a.depno = b.depno
11 where a.id > 4800000
12 order by a.id limit 25;
13 受影响的行: 0
14 时间: 0.000s

这个效率是最好的,无论怎么分页,耗时基本都是一致的,因为他执行完条件之后,都只扫描了25条数据。

但是有个问题,只适合一页一页的分页,这样才能记住前一个分页的最后Id。如果用户跳着分页就有问题了,比如刚刚刷完第25页,马上跳到35页,数据就会不对。

这种的适合场景是类似百度搜索或者腾讯新闻那种滚轮往下拉,不断拉取不断加载的情况。这种延迟加载会保证数据不会跳跃着获取。

3、降级策略

看了网上一个阿里的dba同学分享的方案:配置limit的偏移量和获取数一个最大值,超过这个最大值,就返回空数据。

因为他觉得超过这个值你已经不是在分页了,而是在刷数据了,如果确认要找数据,应该输入合适条件来缩小范围,而不是一页一页分页。

这个跟我同事的想法大致一样:request的时候 如果offset大于某个数值就先返回一个4xx的错误。

小结

当晚我们应用上述第三个方案,对offset做一下限流,超过某个值,就返回空值。第二天使用第一种和第二种配合使用的方案对程序和数据库脚本进一步做了优化。

合理来说做任何功能都应该考虑极端情况,设计容量都应该涵盖极端边界测试。另外,关注公众号互联网架构师,在后台回复:2T,可以获取我整理的 Java 系列面试题和答案,非常齐全。

另外,该有的限流、降级也应该考虑进去。比如工具多线程调用,在短时间频率内8000次调用,可以使用计数服务判断并反馈用户调用过于频繁,直接给予断掉。

哎,大意了啊,搞了半夜,QA同学不讲武德。

不过这是很美好的经历了。

同事乱用分页 MySQL 卡爆,我真是醉了...相关推荐

  1. 什么是计算机频繁读写硬盘,电脑经常卡爆?一文秒懂硬盘占用100%的原因

    原标题:电脑经常卡爆?一文秒懂硬盘占用100%的原因 很多人问存储极客(微信公众号:SSDGeek),为什么有时候硬盘读写速度明明不高,占用率却达到100%,电脑用起来卡卡的? 如上图中这样,0.1M ...

  2. 手机技巧:微信这个“设置”建议关闭!否则不到半年就卡爆了

    我们知道手机分为两大派系,一个是以苹果为首的IOS系统,另一个则是以安卓系统为首的各大品牌手机.那么这两者有着众多的差异,其中苹果的IOS系统有一个闪光点,却是安卓系统达不到的高度,那就是手机流畅度的 ...

  3. dnf计算机配置检测,如何用DNF的方式来测试(卡爆)''你的电脑''

    原标题:如何用DNF的方式来测试(卡爆)''你的电脑'' 如何用DNF的方式来测试''你的电脑配置'',废话不多说我们直接进入正题,电脑不好的玩家就别皮辣 既然是测试,肯定要把配置拉满 基础设置拉满后 ...

  4. wsappx把电脑卡爆了解决办法

    有用户电脑用着用着,发现越来越卡,经过一番检查发现是wsappx的问题,那么wsappx把电脑卡爆了关不掉要如何解决?下面小编就给大家带来详细的解决办法啦. 方法一: 1.首先我们打开"微软 ...

  5. 卡爆代码%0|%0原理详解

    不是吧不是吧,我刚敲完标题就有一位"精神"小伙问我%0|%0是什么... 好吧,那我略用篇幅解释一下-- 其实简而言之就是一个简单的-- 木马 ! 如何"制作" ...

  6. eclipse打开js等文件CPU卡爆解决办法

    eclipse打开js等文件CPU卡爆有效解决办法: 修改eclipse.ini文件中的eclipse参数

  7. 编译卡爆的Android Studio 3.1.1

    编译卡爆的Android Studio 3.1.1 手贱把 Android Studio 版本更新到了 3.1.1,结果就是打开项目后一直在编译-编译-. 赶快百度了一下,下面是大神们的表述: and ...

  8. 程序Bug,卡爆的定位分析

    最近这几天嵌入公司的算法库,发现程序会卡死现象,可是我个人的程序一直跑得很稳定,只是更新了新的算法sdk,导致程序卡爆了,但是有几个奇怪的现场 1.在线跑的时候一下子就卡死了,导致程序无法进行,并且内 ...

  9. 解决Android studio编译大文件,狂占内存、卡爆的问题

    本人使用Android studio3.1进行NDK/JNI开发,遇到编译一个11.4M的巨大.h文件(储存一个5万行的数组),编译器默认1024M内存,完全不够用,编译后狂战内存直至卡爆. 解决方法 ...

  10. 同事乱用 Redis 卡爆,我真是醉了...

    欢迎关注方志朋的博客,回复"666"获面试宝典 来源:my.oschina.net/xiaomu0082/blog/2990388 首先说下问题现象:内网sandbox环境API持 ...

最新文章

  1. MySQL 5.6通过Keepalived+互为主从实现高可用架构
  2. NETCF平台下利用XmlSerializer对于复杂类型序列化的探索(三)
  3. 回归素材(part7)--机器学习入门到实战-MATLAB实践应用
  4. 24点游戏java_使用java编写计算24点游戏程序
  5. Spring 官方又孵化了个顶级项目,或将改变前后端API现状!
  6. win7下如何快速打开便笺或便签实用小工具
  7. 南航计算机科学与技术学院老师,南航计算机科学与技术学院导师介绍:孙涵
  8. balenaEtcher-1.5.70可能是最好用的镜像写U盘工具 img to usb dmg to usb支持多种格式内附截图介绍多平台均有
  9. 猿创征文 | 2022 我的开发者工具
  10. 转载_CSR867x — 说说什么是ANC、CVC、DSP降噪
  11. Ubuntu(debian) 程序 dep 打包
  12. 腾讯QQ会员中心g_tk32算法【C#版】
  13. JAVA中Action层, Service层 ,modle层 和 Dao层的功能区分
  14. 磁盘阵列和存储服务器的区别
  15. 学习ofbiz 订单支付设计
  16. android文件恢复功能,安卓手机恢复删除文件,如何恢复
  17. 【备忘】Java从零到精通学习路线培训教程
  18. Debian 修改系统语言
  19. pyspark.sql.functions.lit(col)
  20. 虚拟机VM中如何安装图形化界面?

热门文章

  1. iOS底层探索之多线程(五)—GCD不同队列源码分析
  2. 利用iMazing备份功能替换游戏存档
  3. What?什么是区块链?你不知道就太low了
  4. pg数据库表接口和数据导出
  5. 美国欲投 2.58 亿美元与中国争夺超算霸主地位
  6. 线程阻塞问题-功能:环信登录失败后自动登录5次
  7. Exchange 2010和Exchange 2016共存部署-5:向导安装EX16邮箱服务器
  8. 页面显示正常,控制台报错
  9. Mac下Android相关配置
  10. 手工删除oracle的方法