精品国产人成在线_亚洲高清无码在线观看_国产在线视频国产永久2021_国产AV综合第一页一个的一区免费影院黑人_最近中文字幕MV高清在线视频

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

就這么個破分頁,我踩了三次大坑!

小林coding ? 來源:小林coding ? 2023-11-27 16:47 ? 次閱讀

之前踩到一個比較無語的生產 BUG,嚴格來說其實也不能算是 BUG,只能說開發同事對于業務同事的需求理解沒有到位。

這個 BUG 其實和分頁沒有任何關系,但是當我去排查問題的時候,我看了一眼 SQL ,大概是這樣的:

select * from table order by priority limit 1;

priority,就是優先級的意思。

按照優先級 order by 然后 limit 取優先級最高(數字越小,優先級越高)的第一條 ,結合業務背景和數據庫里面的數據,我立馬就意識到了問題所在。

想起了我當年在寫分頁邏輯的時候,雖然場景和這個完全不一樣,但是踩過到底層原理一模一樣的坑,這玩意印象深刻,所以立馬就識別出來了。

借著這個問題,也盤點一下遇到過的三個關于分頁查詢有意思的坑

職業生涯的第一個生產 BUG

職業生涯的第一個生產 BUG 就是一個小小的分頁查詢。

當時還在做支付系統,接手的一個需求也很簡單就是做一個定時任務,定時把數據庫里面狀態為初始化的訂單查詢出來,調用另一個服務提供的接口查詢訂單的狀態并更新。

由于流程上有數據強校驗,不用考慮數據不存在的情況。所以該接口可能返回的狀態只有三種:成功,失敗,處理中。

很簡單,很常規的一個需求對吧,我分分鐘就能寫出偽代碼:

//獲取訂單狀態為初始化的數據(0:初始化1:處理中2:成功3:失敗)
//select*fromorderwhereorder_status=0;
ArrayListinitOrderInfoList=queryInitOrderInfoList();
//循環處理這批數據
for(OrderInfoorderInfo:initOrderInfoList){
//捕獲異常以免一條數據錯誤導致循環結束
try{
//發起rpc調用
StringorderStatus=queryOrderStatus(orderInfo.getOrderId);
//更新訂單狀態
updateOrderInfo(orderInfo.getOrderId,orderStatus);
}catch(Exceptione){
//打印異常
}
}

來,你說上面這個程序有什么問題?

其實在絕大部分情況下都沒啥大問題,數據量不多的情況下程序跑起來沒有任何毛病。

但是,如果數據量多起來了,一次性把所有初始化狀態的訂單都拿出來,是不是有點不合理了,萬一把內存給你撐爆了怎么辦?

所以,在我已知數據量會很大的情況下,我采取了分批次獲取數據的模式,假設一次性取 100 條數據出來玩。

那么 SQL 就是這樣的:

select * from order where order_status=0 order by create_time limit 100;

所以上面的偽代碼會變成這樣:

while(true){
//獲取訂單狀態為初始化的數據(0:初始化1:處理中2:成功3:失敗)
//select*fromorderwhereorder_status=0orderbycreate_timelimit100;
ArrayListinitOrderInfoList=queryInitOrderInfoList();
//循環處理這批數據
for(OrderInfoorderInfo:initOrderInfoList){
//捕獲異常以免一條數據錯誤導致循環結束
try{
//發起rpc調用
StringorderStatus=queryOrderStatus(orderInfo.getOrderId);
//更新訂單狀態
updateOrderInfo(orderInfo.getOrderId,orderStatus);
}catch(Exceptione){
//打印異常
}
}
}

來,你又來告訴我上面這一段邏輯有什么問題?

作為程序員,我們看到 while(true) 這樣的寫法立馬就要警報拉滿,看看有沒有死循環的風險。

那你說上面這段代碼在什么時候退不出來?

當有任何一條數據的狀態沒有從初始化變成成功、失敗或者處理中的時候,就會導致一直循環。

而雖然發起 RPC 調用的地方,服務提供方能確保返回的狀態一定是成功、失敗、處理中這三者之中的一個,但是這個有一個前提是接口調用正常的情況下。

如果接口調用一旦異常,那么按照上面的寫法,在拋出異常后,狀態并未發生變化,還會是停留在“初始化”,從而導致死循環。

當年,測試同學在測試階段直接就測出了這個問題,然后我對其進行了修改。

我改變了思路,把每次分批次查詢 100 條數據,修改為了分頁查詢,引入了 PageHelper 插件:

//是否是最后一頁
while(pageInfo.isLastPage){
pageNum=pageNum+1;
//獲取訂單狀態為初始化的數據(0:初始化1:處理中2:成功3:失敗)
//select*fromorderwhereorder_status=0orderbycreate_timelimitpageNum*100,100;
PageHelper.startPage(pageNum,100);
ArrayListinitOrderInfoList=queryInitOrderInfoList();
pageInfo=newPageInfo(initOrderInfoList);
//循環處理這批數據
for(OrderInfoorderInfo:initOrderInfoList){
//捕獲異常以免一條數據錯誤導致循環結束
try{
//發起rpc調用
StringorderStatus=queryOrderStatus(orderInfo.getOrderId);
//更新訂單狀態
updateOrderInfo(orderInfo.getOrderId,orderStatus);
}catch(Exceptione){
//打印異常
}
}
}

跳出循環的條件為判斷當前頁是否是最后一頁。

由于每循環一次,當前頁就加一,那么理論上講一定會是翻到最后一頁的,沒有任何毛病,對不對?

我們可以分析一下上面的代碼邏輯。

假設,我們有 120 條 order_status=0 的數據。

那么第一頁,取出了 100 條數據:

SELECT * from order_info WHERE order_status=0 LIMIT 0,100;

這 100 條處理完成之后,第二頁還有數據嗎?

第二頁對應的 sql 為:

SELECT * from order_info WHERE order_status=0 LIMIT 100,100;

但是這個時候,狀態為 0 的數據,只有 20 條了,而分頁要從第 100 條開始,是不是獲取不到數據,導致遺漏數據了?

確實一定會翻到最后一頁,解決了死循環的問題,但又有大量的數據遺漏怎么辦呢?

當時我苦思冥想,想到一個辦法:導致數據遺漏的原因是因為我在翻頁的時候,數據狀態在變化,導致總體數據在變化。

那么如果我每次都從后往前取數據,每次都固定取最后一頁,能取到數據就代表還有數據要處理,循環結束條件修改為“當前頁即是第一頁,也是最后一頁時”就結束,這樣不就不會遺漏數據了?

我再給你分析一下。

假設,我們有 120 條 order_status=0 的數據,從后往前取了 100 天出來進行出來,有 90 條處理成功,10 條的狀態還是停留在“處理中”。

第二次再取的時候,會把剩下的 20 條和這次“處理中”的 10 條,共計 30 條再次取出來進行處理。

確保沒有數據遺漏。

后來測試環節驗收通過了,這個方案上線之后,也確實沒有遺漏過數據了。

直到后來又一天,提供 queryOrderStatus 接口的服務異常了,我發過去的請求超時了。

導致我取出來的數據,每一條都會拋出異常,都不會更新狀態。從而導致我每次從后往前取數據,都取到的是同一批數據。

從程序上的表現上看,日志瘋狂的打印,但是其實一直在處理同一批,就是死循環了。

好在我當時還在新手保護期,領導幫我扛下來了。

最后隨著業務的發展,這塊邏輯也完全發生了變化,邏輯由我們主動去調用 RPC 接口查詢狀態變成了,下游狀態變化后進行 MQ 主動通知,所以我這一坨騷代碼也就隨之光榮下崗。

我現在想了一下,其實這個場景,用分頁的思想去取數據真的不好做。

還不如用最開始的分批次的思想,只不過在會變化的“狀態”之外,再加上另外一個不會改變的限定條件,比如常見的創建時間:

select * from order where order_status=0 and create_time>xxx order by create_time limit 100;

最好不要基于狀態去做分頁,如果一定要基于狀態去做分頁,那么要確保狀態在分頁邏輯里面會扭轉下去。

這就是我職業生涯的第一個生產 BUG,一個低級的分頁邏輯錯誤。

還是分頁,又踩到坑

這也是在工作的前兩年遇到的一個關于分頁的坑。

最開始在學校的時候,大家肯定都手擼過分頁邏輯,自己去算總頁數,當前頁,頁面大小啥的。

當時功力尚淺,覺得這部分邏輯寫起來是真復雜,但是扣扣腦袋也還是可以寫出來。

后來參加工作了之后,在項目里面看到了 PageHelper 這個玩意,了解之后發了“斯國一”的驚嘆:有了這玩意,誰還手寫分頁啊。

但是我在使用 PageHelper 的時候,也踩到過一個經典的“坑”。

最開始的時候,代碼是這樣的:

PageHelper.startPage(pageNum,100);
Listlist=orderInfoMapper.select(param1);

后來為了避免不帶 where 條件的全表查詢,我把代碼修改成了這樣:

PageHelper.startPage(pageNum,100);
if(param!=null){
Listlist=orderInfoMapper.select(param);
}

然后,隨著程序的迭代,就出 BUG 了。因為有的業務場景下,param 參數一路傳遞進來之后就變成了 null。

但是這個時候 PageHelper 已經在當前線程的 ThreadLocal 里面設置了分頁參數了,但是沒有被消費,這個參數就會一直保留在這個線程上,也就是放在線程的 ThreadLocal 里面。

當這個線程繼續往后跑,或者被復用的時候,遇到一條 SQL 語句時,就可能導致不該分頁的方法去消費這個分頁參數,產生了莫名其妙的分頁。

所以,上面這個代碼,應該寫成下面這個樣子:

if(param!=null){
PageHelper.startPage(pageNum,100);
Listlist=orderInfoMapper.select(param);
}

也是這次踩坑之后,我翻閱了 PageHelper 的源碼,了解了底層原理,并總結了一句話:需要保證在 PageHelper 方法調用后緊跟 MyBatis 查詢方法,否則會污染線程。

在正確使用 PageHelper 的情況下,其插件內部,會在 finally 代碼段中自動清除了在 ThreadLocal 中存儲的對象。

這樣就不會留坑。

這次翻頁源碼的過程影響也是比較深刻的,雖然那個時候經驗不多,但是得益于 MyBatis 的源碼和 PageHelper 的源碼寫的都非常的符合正常人的思維,閱讀起來門檻不高,再加上我有具體的疑問,所以那是一次古早時期,尚在新手村時,為數不多的,閱讀源碼之后,感覺收獲滿滿的經歷。

分頁丟數據

關于這個 BUG 可以說是印象深刻了。

當年遇到這個坑的時候排查了很長時間沒啥頭緒,最后還是組里的大佬指了條路。

業務需求很簡單,就是在管理頁面上可以查詢訂單列表,查詢結果按照訂單的創建時間倒序排序。

對應的分頁 SQL 很簡單,很常規,沒有任何問題:

select * from table order by create_time desc limit 0,10;

但是當年在頁面上的表現大概是這樣的:

b35f7ca4-8cf9-11ee-939d-92fbcf53809c.png

訂單編號為 5 的這條數據,會同時出現在了第一頁和第二頁。

甚至有的數據在第二頁出現了之后,在第五頁又出現一次。

后來定位到產生這個問題的原因是因為有一批數量不小的訂單數據是通過線下執行 SQL 的方式導入的。

而導入的這一批數據,寫 SQL 的同學為了方便,就把 create_time 都設置為了同一個值,比如都設置為了 2023-09-10 1256 這個時間。

由于 create_time 又是我作為 order by 的字段,當這個字段的值大量都是同一個值的時候,就會導致上面的一條數據在不同的頁面上多次出現的情況。

針對這個現象,當時組里的大佬分析明白之后,扔給我一個鏈接:

https://dev.mysql.com/doc/refman/5.7/en/limit-optimization.html

這是 MySQL 官方文檔,這一章節叫做“對 Limit 查詢的優化”。

開篇的時候人家就是這樣說的:

b3698b72-8cf9-11ee-939d-92fbcf53809c.png

如果將 LIMIT row_count 和 ORDER BY 組合在一起,那么 MySQL 在找到排序結果的第一行 count 行時就停止排序,而不是對整個結果進行排序。

然后給了這一段補充說明:

b377becc-8cf9-11ee-939d-92fbcf53809c.png

如果多條記錄的 ORDER BY 列中有相同的值,服務器可以自由地按任何順序返回這些記錄,并可能根據整體執行計劃的不同而采取不同的方式。

換句話說,相對于未排序列,這些記錄的排序順序是 nondeterministic 的:

b3813a74-8cf9-11ee-939d-92fbcf53809c.png

然后官方給了一個示例。

首先,不帶 limit 的時候查詢結果是這樣的:

b3918532-8cf9-11ee-939d-92fbcf53809c.png

基于這個結果,如果我要取前五條數據,對應的 id 應該是 1,5,3,4,6。

但是當我們帶著 limit 的時候查詢結果可能是這樣的:

b3a22ca2-8cf9-11ee-939d-92fbcf53809c.png

對應的 id 實際是 1,5,4,3,6。

這就是前面說的:如果多條記錄的 ORDER BY 列中有相同的值,服務器可以自由地按任何順序返回這些記錄,并可能根據整體執行計劃的不同而采取不同的方式。

從程序上的表現上來看,結果就是 nondeterministic。

所以看到這里,我們大概可以知道我前面遇到的分頁問題的原因是因為那一批手動插入的數據對應的 create_time 字段都是一樣的,而 MySQL 這邊又對 Limit 參數做了優化,運行結果出現了不確定性,從而頁面上出現了重復的數據。

而回到文章最開始的這個 SQL,也就是我一眼看出問題的這個 SQL:

select * from table order by priority limit 1;

因為在我們的界面上,只是約定了數字越小優先級越高,數字必須大于 0。

所以當大家在輸入優先級的時候,大部分情況下都默認自己編輯的數據對應的優先級最高,也就是設置為 1,從而導致數據庫里面有大量的優先級為 1 的數據。

而程序每次處理,又只會按照優先級排序只會,取一條數據出來進行處理。

經過前面的分析我們可以知道,這樣取出來的數據,不一定每次都一樣。

所以由于有這段代碼的存在,導致業務上的表現就很奇怪,明明是一模一樣的請求參數,但是最終返回的結果可能不相同。

好,現在,我問你,你說在前面,我給出的這樣的分頁查詢的 SQL 語句有沒有毛病?

select * from table order by create_time desc limit 0,10;

沒有任何毛病嘛,執行結果也沒有任何毛病?

有沒有給你按照 create_time 排序?

摸著良心說,是有的。

有沒有給你取出排序后的 10 條數據?

也是有的。

所以,針對這種現象,官方的態度是:我沒錯!在我的概念里面,沒有“分頁”這樣的玩意,你通過組合我提供的功能,搞出了“分頁”這種業務場景,現在業務場景出問題了,你反過來說我底層有問題?

這不是欺負老實人嗎?我沒錯!

所以,官方把這兩種案例都拿出來,并且強調:

在每種情況下,查詢結果都是按 ORDER BY 的列進行排序的,這樣的結果是符合 SQL 標準的。

b3ccf69e-8cf9-11ee-939d-92fbcf53809c.png

雖然我沒錯,但是我還是可以給你指個路。

如果你非常在意執行結果的順序,那么在 ORDER BY 子句中包含一個額外的列,以確保順序具有確定性。

例如,如果 id 值是唯一的,你可以通過這樣的排序使給定類別值的行按 id 順序出現。

你這樣去寫,排序的時候加個 id 字段,就穩了:

b3e51652-8cf9-11ee-939d-92fbcf53809c.png

好了,本文的技術部分就到這里啦。


聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 數據
    +關注

    關注

    8

    文章

    6898

    瀏覽量

    88840
  • 代碼
    +關注

    關注

    30

    文章

    4751

    瀏覽量

    68359
  • BUG
    BUG
    +關注

    關注

    0

    文章

    155

    瀏覽量

    15653

原文標題:就這么個破分頁,我踩了三次大坑!

文章出處:【微信號:小林coding,微信公眾號:小林coding】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    講一講的TCP三次握手和四揮手

    如果你學過網絡基礎知識,那么你一定對TCP三次握手不陌生。今天想用通俗的話來給大家講一講TCP三次握手和四揮手。畢竟,這個知識點在面試時被問到的概率很高!
    的頭像 發表于 02-03 10:43 ?2681次閱讀
    講一講的TCP<b class='flag-5'>三次</b>握手和四<b class='flag-5'>次</b>揮手

    verilog實現三次樣條插值

    本帖最后由 來看看你在干什么 于 2018-5-15 09:10 編輯 用verilog實現EMD算法,需要用到三次樣條插值法,請問有做過類似算法實現的嗎,可以講一下verilog實現三次樣條插值的思路,或者相互交流探討
    發表于 05-13 21:34

    三次諧波和五諧波失真嚴重是由哪些原因造成的?

    各位前輩,最近初學DSM,搭了一DT的2階CIFB調制器,但是出現三次諧波和五諧波失真嚴重的問題,想請教一下前輩們主要是由哪調制器些
    發表于 06-24 07:15

    低壓三次諧波濾波器(TSF)

    低壓三次諧波濾波器(TSF)非線性的單相負荷會在接入相與中性線之間產生三次諧波電流,并在中性線上疊加,造成電流和電壓畸變。三次諧波電流會引起中性線過載,還會形成1
    發表于 12-31 19:28 ?39次下載

    中國IT制造業面臨“第三次大變革”

    IT制造業面臨第三次大變革?這個幾年前還是天方夜譚的話題,如今卻在逐步變成現實。
    發表于 06-06 08:45 ?954次閱讀

    TCP三次握手的過程描述

    本文檔主要描述TCP三次握手的過程,一完整的三次握手也就是 請求---應答---再次確認
    發表于 03-02 15:37 ?8次下載

    關于三次諧波電流治理方案的淺析

    現代大量LED燈、LED屏使用,而其電源為開關電源。開關電源的輸入端為整流電路,整流是典型的的諧波源,其中單相橋式整流電路的諧波電流有3、5、7、9等。這里面就涉及到我們所談的
    發表于 06-30 18:06 ?3255次閱讀
    關于<b class='flag-5'>三次</b>諧波電流治理方案的淺析

    三次諧波是什么,三次諧波會造成哪些影響

    正弦波形分量其頻率為基波(頻率為50Hz)的倍,在物理學和電類學科中都有三次諧波的概念 任何一波函數都可以進行傅里葉分解:f(t)=(k=1,n)cos(kwt+ak),當k=1時的分量f(t)=cos(wt+a)成為基波分
    發表于 11-16 15:44 ?3w次閱讀
    <b class='flag-5'>三次</b>諧波是什么,<b class='flag-5'>三次</b>諧波會造成哪些影響

    基于點插值的三次PH曲線構造方法

    為推廣三次PH曲線的實際應用,研究在給定3平面型值點條件下的三次PH曲線構造方法。三次PH曲線具有鮮明的幾何性質和代數特征,采用平面參數曲線的復數表示方法,
    發表于 06-04 11:40 ?16次下載

    射頻識別技術漫談(12)——三次相互認證

    射頻識別技術漫談(12)——三次相互認證
    的頭像 發表于 10-11 16:19 ?1256次閱讀
    射頻識別技術漫談(12)——<b class='flag-5'>三次</b>相互認證

    說說TCP三次握手的過程?為什么是三次而不是兩、四

    三次而不是兩或四。 首先,我們需要了解TCP是一種面向連接的協議。在進行數據傳輸之前,發送端和接收端需要建立一可靠的連接。TCP三次
    的頭像 發表于 02-04 11:03 ?621次閱讀

    諧波和三次諧波區別 二諧波危害沒有三次諧波大?

    諧波和三次諧波區別 二諧波危害沒有三次諧波大? 在現代電力系統中,諧波問題逐漸引起人們的關注。諧波是指頻率是基波頻率的倍數的電流或電壓成分。二
    的頭像 發表于 04-08 17:11 ?5262次閱讀

    三次諧波對注入式定子接地影響

    引言 隨著電力系統的快速發展,電力系統的諧波問題日益突出。三次諧波作為電力系統中常見的一種諧波,對電力系統的安全穩定運行產生了一定的影響。特別是在注入式定子接地系統中,三次諧波的影響尤為明顯。 三次
    的頭像 發表于 07-25 14:55 ?597次閱讀

    三次諧波定子接地保護動作條件

    三次諧波定子接地保護是電力系統中一種重要的保護方式,主要用于保護發電機、變壓器等設備的定子繞組。 一、三次諧波定子接地保護的基本原理 1.1 三次諧波的產生 在電力系統中,由于非線性負載、變壓器鐵芯
    的頭像 發表于 07-25 14:57 ?925次閱讀

    簡述TCP協議的三次握手機制

    機制是建立一可靠的連接的關鍵步驟。以下是對TCP協議三次握手機制的介紹: 概述 TCP協議的三次握手機制是一種用于在兩通信實體之間建立連接的過程。這個過程確保
    的頭像 發表于 08-16 10:57 ?677次閱讀