QA 不講武德!線上 1 億+ 數據亂分頁,讓我搞到半夜。。

来源:https://www.cnblogs.com/javastack/archive/2023/02/12/17114362.html
-Advertisement-
Play Games

作者:翁智華 出處:https://www.cnblogs.com/wzh2010/ 背景 一天晚上10點半,下班後愉快的坐在在回家的地鐵上,心裡想著周末的生活怎麼安排。 突然電話響了起來,一看是我們的一個開發同學,頓時緊張了起來,本周的版本已經發佈過了,這時候打電話一般來說是線上出問題了。 果然, ...


作者:翁智華

出處: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 BEGIN
 6     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 BEGIN
 6     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 BEGIN
 6     DECLARE i INT DEFAULT 0;
 7     SET autocommit = 0;
 8     REPEAT
 9     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.depname
 3 from emp a left join dep b on a.depno = b.depno order by a.id desc limit 100,25;
 4 受影響的行: 0
 5 時間: 0.001s
 6 [SQL]
 7 SELECT a.empno,a.empname,a.job,a.sal,b.depno,b.depname
 8 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

因為掃描的數據多,所以這個明顯不是一個量級上的耗時。

解決方案

1、使用索引覆蓋+子查詢優化

因為我們有主鍵id,並且在上面建了索引,所以可以先在索引樹中找到開始位置的 id值,再根據找到的id值查詢行數據。

 1 /*子查詢獲取偏移100條的位置的id,在這個位置上往後取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
 4 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.depname
 9 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.depname
 3 from emp a left join dep b on a.depno = b.depno
 4 where a.id >= (select id from emp order by id limit 100,1)
 5 order by a.id limit 25;
 6 受影響的行: 0
 7 時間: 0.106s
 8  
 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.depname
 3 from emp a left join dep b on a.depno = b.depno
 4 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.depname
 8 from emp a left join dep b on a.depno = b.depno
 9 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.depname
 3 from emp a left join dep b on a.depno = b.depno
 4 where a.id > 100 order by a.id limit 25;
 5 受影響的行: 0
 6 時間: 0.001s
 7  
 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做一下限流,超過某個值,就返回空值。第二天使用第一種和第二種配合使用的方案對程式和資料庫腳本進一步做了優化。

合理來說做任何功能都應該考慮極端情況,設計容量都應該涵蓋極端邊界測試。

另外,該有的限流、降級也應該考慮進去。比如工具多線程調用,在短時間頻率內8000次調用,可以使用計數服務判斷並反饋用戶調用過於頻繁,直接給予斷掉。

哎,大意了啊,搞了半夜,QA同學不講武德。不過這是很美好的經歷了。

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 使用JS的DOM(文檔對象模型)獲取前端迴圈的參數 使用Go語言渲染html,但是想讓網頁動起來,顯示一些彈窗還是比較麻煩的,於是乎,想到使用js獲取頁面的數據進行顯示,但是js無法載入go的一些變數。想了很久,突然在網頁調試的時候使用了js的DOM進行元素查找獲得了些許靈感最後實現了這個功能。 1 ...
  • 初探富文本之CRDT協同演算法 CRDT的英文全稱是Conflict-free Replicated Data Type,最初是由協同文本編輯和移動計算而發展的,現在還被用作線上聊天系統、音頻分發平臺等等。當前CRDT演算法在富文本編輯器領域的協同依舊是典型的場景,常用於作為實現文檔協同的底層演算法,支持 ...
  • 0x001 自定義參數 $extra_param = "Some extra param"; Hook::add('response_end', function($params) use ($extra_param) { Log::info('Extra: '.$extra_param); }); ...
  • 官網: https://spring.io/ 更多spring cloud zookeeper 參考 https://docs.spring.io/spring-cloud-zookeeper/docs/current/reference/html 左側菜單 向下找到 spring Cloud Zo ...
  • 這篇文章主要介紹分散式系統中的非集中式結構,以及我們經常使用的三種基於非集中式架構的解決方案:Akka集群、Redis集群和Cassandra集群。 ...
  • SpringMVC底層機制簡單實現-03 https://github.com/liyuelian/springmvc-demo.git 7.任務6-完成控制器方法獲取參數-@RequestParam 功能說明:自定義 @RequestParam 註解和方法參數名獲取參數。 當瀏覽器訪問 Handl ...
  • 最近項目 需要用到 sybase(sql anywhere)、mysql 資料庫 兩邊數據交互 。由於之前對sybase 資料庫一點不懂 踩了許多坑 特意記下: 連接 sybase 客戶端需要用到 SQL Central 17.0 (64-bit) 這個工具; 接下來我會一步一步詳細介紹: 1.連接 ...
  • 雪花演算法的原理 第一位符號位固定為0,41位時間戳,10位workld,12位序列號,位數可以有不同實現優點: 每個毫秒值包含的ID值很多,不夠可以變動位數來增加,性能佳 (依賴workld的實現)。.時間戳值在高位,中間是固定的機器碼,自增的序列在低位,整個ID是趨勢遞增的。0能夠根據業務場景數據 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...