<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一次線上MySQL分頁(yè)事故,搞了半夜...

          共 4872字,需瀏覽 10分鐘

           ·

          2022-04-28 21:56

          往期熱門文章:

          1、因?yàn)樵燧喿?我一個(gè)月就轉(zhuǎn)正了
          2、用了Stream后,代碼反而越寫(xiě)越丑?
          3、一不小心節(jié)約了 591 臺(tái)機(jī)器!
          4、你見(jiàn)過(guò)哪些目瞪口呆的 Java 代碼技巧?
          5、笑死!程序員延壽指南開(kāi)源了

          文章來(lái)源:【公眾號(hào):JAVA日知錄】


          目錄

          • 背景

          • 分析

          • 數(shù)據(jù)模擬

          • 測(cè)試

          • 解決方案

          • 小結(jié)


          今天給大家分享個(gè)生產(chǎn)事故,一個(gè)由于 MySQL 分頁(yè)導(dǎo)致的線上事故,事情是這樣的~


          背景


          一天晚上 10 點(diǎn)半,下班后愉快的坐在在回家的地鐵上,心里想著周末的生活怎么安排。


          突然電話響了起來(lái),一看是我們的一個(gè)運(yùn)維同學(xué),頓時(shí)緊張了起來(lái),本周的版本已經(jīng)發(fā)布過(guò)了,這時(shí)候打電話一般來(lái)說(shuō)是線上出問(wèn)題了。


          果然,溝通的情況是線上的一個(gè)查詢數(shù)據(jù)的接口被瘋狂的失去理智般的調(diào)用,這個(gè)操作直接導(dǎo)致線上的 MySQL 集群被拖慢了。


          好吧,這問(wèn)題算是嚴(yán)重了,匆匆趕到家后打開(kāi)電腦,跟同事把 Pinpoint 上的慢查詢?nèi)罩緭瞥鰜?lái)。


          看到一個(gè)很奇怪的查詢,如下:
          1?POST??domain/v1.0/module/method?order=condition&orderType=desc&offset=1800000&limit=500


          domain、module 和 method 都是化名,代表接口的域、模塊和實(shí)例方法名,后面的 offset 和 limit 代表分頁(yè)操作偏移量和每頁(yè)的數(shù)量,也就是說(shuō)該同學(xué)是在翻第(1800000/500+1=3601)頁(yè)。初步撈了一下日志,發(fā)現(xiàn)有 8000 多次這樣調(diào)用。


          這太神奇了,而且我們頁(yè)面上的分頁(yè)單頁(yè)數(shù)量也不是 500,而是 25 條每頁(yè),這個(gè)絕對(duì)不是人為的在功能頁(yè)面上進(jìn)行一頁(yè)一頁(yè)的翻頁(yè)操作,而是數(shù)據(jù)被刷了(說(shuō)明下,我們生產(chǎn)環(huán)境數(shù)據(jù)有 1 億+)。


          詳細(xì)對(duì)比日志發(fā)現(xiàn),很多分頁(yè)的時(shí)間是重疊的,對(duì)方應(yīng)該是多線程調(diào)用。


          通過(guò)對(duì)鑒權(quán)的 Token 的分析,基本定位了請(qǐng)求是來(lái)自一個(gè)叫做 ApiAutotest 的客戶端程序在做這個(gè)操作,也定位了生成鑒權(quán) Token 的賬號(hào)來(lái)自一個(gè) QA 的同學(xué)。立馬打電話給同學(xué),進(jìn)行了溝通和處理。


          分析


          其實(shí)對(duì)于我們的 MySQL 查詢語(yǔ)句來(lái)說(shuō),整體效率還是可以的,該有的聯(lián)表查詢優(yōu)化都有,該簡(jiǎn)略的查詢內(nèi)容也有,關(guān)鍵條件字段和排序字段該有的索引也都在,問(wèn)題在于他一頁(yè)一頁(yè)的分頁(yè)去查詢,查到越后面的頁(yè)數(shù),掃描到的數(shù)據(jù)越多,也就越慢。


          我們?cè)诓榭辞皫醉?yè)的時(shí)候,發(fā)現(xiàn)速度非常快,比如 ?limit 200,25,瞬間就出來(lái)了。但是越往后,速度就越慢,特別是百萬(wàn)條之后,卡到不行,那這個(gè)是什么原理呢。


          先看一下我們翻頁(yè)翻到后面時(shí),查詢的 sql 是怎樣的:
          1?select?*?from?t_name?where?c_name1='xxx'?order?by?c_name2?limit?2000000,25;


          這種查詢的慢,其實(shí)是因?yàn)?limit 后面的偏移量太大導(dǎo)致的。


          比如像上面的 limit 2000000,25,這個(gè)等同于數(shù)據(jù)庫(kù)要掃描出 2000025 條數(shù)據(jù),然后再丟棄前面的 20000000 條數(shù)據(jù),返回剩下 25 條數(shù)據(jù)給用戶,這種取法明顯不合理。

          大家翻看《高性能 MySQL》第六章:查詢性能優(yōu)化,對(duì)這個(gè)問(wèn)題有過(guò)說(shuō)明:分頁(yè)操作通常會(huì)使用 limit 加上偏移量的辦法實(shí)現(xiàn),同時(shí)再加上合適的 order by 子句。


          這會(huì)出現(xiàn)一個(gè)常見(jiàn)問(wèn)題:當(dāng)偏移量非常大的時(shí)候,它會(huì)導(dǎo)致 MySQL 掃描大量不需要的行然后再拋棄掉。


          數(shù)據(jù)模擬


          那好,了解了問(wèn)題的原理,那就要試著解決它了。涉及數(shù)據(jù)敏感性,我們這邊模擬一下這種情況,構(gòu)造一些數(shù)據(jù)來(lái)做測(cè)試。


          ①創(chuàng)建兩個(gè)表:?jiǎn)T工表和部門表
          /*部門表,存在則進(jìn)行刪除?*/
          drop?table?if?EXISTS?dep;
          create?table?dep(
          ????id?int?unsigned?primary?key?auto_increment,
          ????depno?mediumint?unsigned?not?null?default?0,
          ????depname?varchar(20)?not?null?default?"",
          ????memo?varchar(200)?not?null?default?""
          );

          /*員工表,存在則進(jìn)行刪除*/
          drop?table?if?EXISTS?emp;
          create?table?emp(
          ????id?int?unsigned?primary?key?auto_increment,
          ????empno?mediumint?unsigned?not?null?default?0,
          ????empname?varchar(20)?not?null?default?"",
          ????job?varchar(9)?not?null?default?"",
          ????mgr?mediumint?unsigned?not?null?default?0,
          ????hiredate?datetime?not?null,
          ????sal?decimal(7,2)?not?null,
          ????comn?decimal(7,2)?not?null,
          ????depno?mediumint?unsigned?not?null?default?0
          );


          ②創(chuàng)建兩個(gè)函數(shù):生成隨機(jī)字符串和隨機(jī)編號(hào)
          /*?產(chǎn)生隨機(jī)字符串的函數(shù)*/
          DELIMITER?$?
          drop?FUNCTION?if?EXISTS?rand_string;
          CREATE?FUNCTION?rand_string(n?INT)?RETURNS?VARCHAR(255)
          BEGIN
          ????DECLARE?chars_str?VARCHAR(100)?DEFAULT?'abcdefghijklmlopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
          ;
          ????DECLARE?return_str?VARCHAR(255)?DEFAULT?'';
          ????DECLARE?i?INT?DEFAULT?0;
          ????WHILE?i?DO
          ????SET?return_str?=?CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));
          ????SET?i?=?i+1;
          ????END?WHILE;
          ????RETURN?return_str;
          END?$
          DELIMITER;


          /*產(chǎn)生隨機(jī)部門編號(hào)的函數(shù)*/
          DELIMITER?$?
          drop?FUNCTION?if?EXISTS?rand_num;
          CREATE?FUNCTION?rand_num()?RETURNS?INT(5)
          BEGIN
          ????DECLARE?i?INT?DEFAULT?0
          ;
          ????SET?i?=?FLOOR(100+RAND()*10);
          ????RETURN?i;
          END?$
          DELIMITER;


          ③編寫(xiě)存儲(chǔ)過(guò)程,模擬 500W 的員工數(shù)據(jù)
          ?/*建立存儲(chǔ)過(guò)程:往emp表中插入數(shù)據(jù)*/
          ?DELIMITER?$
          ?drop?PROCEDURE?if?EXISTS?insert_emp;
          ?CREATE?PROCEDURE?insert_emp(IN?START?INT(10),IN?max_num?INT(10))
          ?BEGIN
          ?????DECLARE?i?INT?DEFAULT?0;
          ?????/*set?autocommit?=0?把a(bǔ)utocommit設(shè)置成0,把默認(rèn)提交關(guān)閉*/
          ?????SET?autocommit?=?0;
          ?????REPEAT
          ?????SET?i?=?i?+?1;
          ?????INSERT?INTO?emp(empno,empname,job,mgr,hiredate,sal,comn,depno)?VALUES?((START+i),rand_string(6),'SALEMAN',0001,now(),2000,400,rand_num());
          ?????UNTIL?i?=?max_num
          ?????END?REPEAT;
          ?????COMMIT;
          ?END?$
          ?DELIMITER;
          ?/*插入500W條數(shù)據(jù)*/
          ?call?insert_emp(0,5000000);


          ④編寫(xiě)存儲(chǔ)過(guò)程,模擬 120 的部門數(shù)據(jù)
          ?/*建立存儲(chǔ)過(guò)程:往dep表中插入數(shù)據(jù)*/
          ?DELIMITER?$
          ?drop?PROCEDURE?if?EXISTS?insert_dept;
          ?CREATE?PROCEDURE?insert_dept(IN?START?INT(10),IN?max_num?INT(10))
          ?BEGIN
          ?????DECLARE?i?INT?DEFAULT?0;
          ?????SET?autocommit?=?0;
          ?????REPEAT
          ?????SET?i?=?i+1;
          ?????INSERT??INTO?dep(?depno,depname,memo)?VALUES((START+i),rand_string(10),rand_string(8));
          ?????UNTIL?i?=?max_num
          ?????END?REPEAT;
          ?????COMMIT;
          ?END?$
          ?DELIMITER;
          ?/*插入120條數(shù)據(jù)*/
          ?call?insert_dept(1,120);


          ⑤建立關(guān)鍵字段的索引,這邊是跑完數(shù)據(jù)之后再建索引,會(huì)導(dǎo)致建索引耗時(shí)長(zhǎng),但是跑數(shù)據(jù)就會(huì)快一些。
          /*建立關(guān)鍵字段的索引:排序、條件*/
          CREATE?INDEX?idx_emp_id?ON?emp(id);
          CREATE?INDEX?idx_emp_depno?ON?emp(depno);
          CREATE?INDEX?idx_dep_depno?ON?dep(depno);?


          測(cè)試


          測(cè)試數(shù)據(jù):
          /*偏移量為100,取25*/
          SELECT?a.empno,a.empname,a.job,a.sal,b.depno,b.depname
          from?emp?a?left?join?dep?b?on?a.depno?=?b.depno?order?by?a.id?desc?limit?100,25;
          /*偏移量為4800000,取25*/
          SELECT?a.empno,a.empname,a.job,a.sal,b.depno,b.depname
          from?emp?a?left?join?dep?b?on?a.depno?=?b.depno?order?by?a.id?desc?limit?4800000,25;?


          執(zhí)行結(jié)果:
          [SQL]
          SELECT?a.empno,a.empname,a.job,a.sal,b.depno,b.depname
          from?emp?a?left?join?dep?b?on?a.depno?=?b.depno?order?by?a.id?desc?limit?100,25;
          受影響的行:?0
          時(shí)間:?0.001s
          [SQL]
          SELECT?a.empno,a.empname,a.job,a.sal,b.depno,b.depname
          from?emp?a?left?join?dep?b?on?a.depno?=?b.depno?order?by?a.id?desc?limit?4800000,25;
          受影響的行:?0
          時(shí)間:?12.275s


          因?yàn)閽呙璧臄?shù)據(jù)多,所以這個(gè)明顯不是一個(gè)量級(jí)上的耗時(shí)。


          解決方案


          ①使用索引覆蓋+子查詢優(yōu)化


          因?yàn)槲覀冇兄麈I id,并且在上面建了索引,所以可以先在索引樹(shù)中找到開(kāi)始位置的 id 值,再根據(jù)找到的 id 值查詢行數(shù)據(jù)。
          ?/*子查詢獲取偏移100條的位置的id,在這個(gè)位置上往后取25*/
          ?SELECT?a.empno,a.empname,a.job,a.sal,b.depno,b.depname
          ?from?emp?a?left?join?dep?b?on?a.depno?=?b.depno
          ?where?a.id?>=?(select?id?from?emp?order?by?id?limit?100,1)
          ?order?by?a.id?limit?25;

          ?/*子查詢獲取偏移4800000條的位置的id,在這個(gè)位置上往后取25*/
          ?SELECT?a.empno,a.empname,a.job,a.sal,b.depno,b.depname
          ?from?emp?a?left?join?dep?b?on?a.depno?=?b.depno
          ?where?a.id?>=?(select?id?from?emp?order?by?id?limit?4800000,1)
          ?order?by?a.id?limit?25;


          執(zhí)行結(jié)果


          執(zhí)行效率相比之前有大幅的提升:
          ?[SQL]
          ?SELECT?a.empno,a.empname,a.job,a.sal,b.depno,b.depname
          ?from?emp?a?left?join?dep?b?on?a.depno?=?b.depno
          ?where?a.id?>=?(select?id?from?emp?order?by?id?limit?100,1)
          ?order?by?a.id?limit?25;
          ?受影響的行:?0
          ?時(shí)間:?0.106s

          ?[SQL]
          ?SELECT?a.empno,a.empname,a.job,a.sal,b.depno,b.depname
          ?from?emp?a?left?join?dep?b?on?a.depno?=?b.depno
          ?where?a.id?>=?(select?id?from?emp?order?by?id?limit?4800000,1)
          ?order?by?a.id?limit?25;
          ?受影響的行:?0
          ?時(shí)間:?1.541s??


          ②起始位置重定義


          記住上次查找結(jié)果的主鍵位置,避免使用偏移量 offset:
          ?/*記住了上次的分頁(yè)的最后一條數(shù)據(jù)的id是100,這邊就直接跳過(guò)100,從101開(kāi)始掃描表*/
          ?SELECT?a.id,a.empno,a.empname,a.job,a.sal,b.depno,b.depname
          ?from?emp?a?left?join?dep?b?on?a.depno?=?b.depno
          ?where?a.id?>?100?order?by?a.id?limit?25;

          ?/*記住了上次的分頁(yè)的最后一條數(shù)據(jù)的id是4800000,這邊就直接跳過(guò)4800000,從4800001開(kāi)始掃描表*/
          ?SELECT?a.id,a.empno,a.empname,a.job,a.sal,b.depno,b.depname
          ?from?emp?a?left?join?dep?b?on?a.depno?=?b.depno
          ?where?a.id?>?4800000
          ?order?by?a.id?limit?25;


          執(zhí)行結(jié)果:
          [SQL]
          SELECT?a.id,a.empno,a.empname,a.job,a.sal,b.depno,b.depname
          from?emp?a?left?join?dep?b?on?a.depno?=?b.depno
          where?a.id?>?100?order?by?a.id?limit?25;
          受影響的行:?0
          時(shí)間:?0.001s

          [SQL]
          SELECT?a.id,a.empno,a.empname,a.job,a.sal,b.depno,b.depname
          from?emp?a?left?join?dep?b?on?a.depno?=?b.depno
          where?a.id?>?4800000
          order?by?a.id?limit?25;
          受影響的行:?0
          時(shí)間:?0.000s?


          這個(gè)效率是最好的,無(wú)論怎么分頁(yè),耗時(shí)基本都是一致的,因?yàn)樗麍?zhí)行完條件之后,都只掃描了 25 條數(shù)據(jù)。


          但是有個(gè)問(wèn)題,只適合一頁(yè)一頁(yè)的分頁(yè),這樣才能記住前一個(gè)分頁(yè)的最后 id。如果用戶跳著分頁(yè)就有問(wèn)題了,比如剛剛刷完第 25 頁(yè),馬上跳到 35 頁(yè),數(shù)據(jù)就會(huì)不對(duì)。


          這種的適合場(chǎng)景是類似百度搜索或者騰訊新聞那種滾輪往下拉,不斷拉取不斷加載的情況。這種延遲加載會(huì)保證數(shù)據(jù)不會(huì)跳躍著獲取。


          ③降級(jí)策略


          看了網(wǎng)上一個(gè)阿里的 DBA 同學(xué)分享的方案:配置 limit 的偏移量和獲取數(shù)一個(gè)最大值,超過(guò)這個(gè)最大值,就返回空數(shù)據(jù)。


          因?yàn)樗X(jué)得超過(guò)這個(gè)值你已經(jīng)不是在分頁(yè)了,而是在刷數(shù)據(jù)了,如果確認(rèn)要找數(shù)據(jù),應(yīng)該輸入合適條件來(lái)縮小范圍,而不是一頁(yè)一頁(yè)分頁(yè)。


          這個(gè)跟我同事的想法大致一樣:request 的時(shí)候如果 offset 大于某個(gè)數(shù)值就先返回一個(gè) 4xx 的錯(cuò)誤。


          小結(jié)


          當(dāng)晚我們應(yīng)用上述第三個(gè)方案,對(duì) offset 做一下限流,超過(guò)某個(gè)值,就返回空值。第二天使用第一種和第二種配合使用的方案對(duì)程序和數(shù)據(jù)庫(kù)腳本進(jìn)一步做了優(yōu)化。合理來(lái)說(shuō)做任何功能都應(yīng)該考慮極端情況,設(shè)計(jì)容量都應(yīng)該涵蓋極端邊界測(cè)試。


          另外,該有的限流、降級(jí)也應(yīng)該考慮進(jìn)去。比如工具多線程調(diào)用,在短時(shí)間頻率內(nèi) 8000 次調(diào)用,可以使用計(jì)數(shù)服務(wù)判斷并反饋用戶調(diào)用過(guò)于頻繁,直接給予斷掉。


          哎,大意了啊,搞了半夜,QA 同學(xué)不講武德。

          往期熱門文章:

          1、笑死!程序員延壽指南開(kāi)源了
          2、用 Dubbo 傳輸文件?被老板一頓揍!
          3、45 個(gè) Git 經(jīng)典操作場(chǎng)景,專治不會(huì)合代碼!
          4、@Transactional 注解失效的3種原因及解決辦法
          5、小學(xué)生們?cè)贐站講算法,網(wǎng)友:我只會(huì)阿巴阿巴
          6、Spring爆出比Log4j2還大的漏洞?
          7、Objects.equals 有坑!!!
          8、Redis 官方可視化工具,功能真心強(qiáng)大!
          8、xxl-job 和 ElasticJob比,誰(shuí)牛?
          10、推薦好用的 Spring Boot 內(nèi)置工具類

          瀏覽 29
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲欧美日韩动漫 | 性爱大香蕉| 成人大香蕉最新视频 | 日韩一级一级一级黄片 | 久久国产激情视频 |