一、前言
大家好!我是sum墨,一個一線的底層碼農,平時喜歡研究和思考一些技術相關的問題並整理成文,限於本人水平,如果文章和程式碼有表述不當之處,還請不吝賜教。
只要是幹過後臺系統的同學應該都做過分頁查詢吧,前端傳送帶有頁碼(pageNum)和每頁顯示數量(pageSize)的請求,後端根據這些引數來提取並返回相應的資料集。在SpringBoot框架中,經常會使用Mybatis+PageHelper的方式實現這個功能。
但大家可能對分頁合理化
這個詞有點兒陌生,不過應該都遇到過因為它產生的問題。這些問題不會觸發明顯的錯誤,所以大家一般都忽視了這個問題。那麼啥是分頁合理化
,我來舉幾個例子:
它的定義:分頁合理化通常是指後端在處理分頁請求時會自動校正不合理的分頁引數,以確保使用者始終收到有效的資料響應。
1. 請求頁碼超出範圍:
假設資料庫中有100條記錄,每頁展示10條,那麼就應該只有10頁資料。如果使用者請求第11頁,不合理化處理可能會返回一個空的資料集,告訴使用者沒有更多資料。開啟分頁合理化後,系統可能會返回第10頁的資料(即最後一頁的資料),而不是一個空集。
2. 請求頁碼小於1:
使用者請求的頁碼如果是0或負數,這在分頁上下文中是沒有意義的。開啟分頁合理化後,系統會將這種請求的頁碼調整為1,返回第一頁的資料。
3. 請求的資料大小小於1:
如果使用者請求的資料大小為0或負數,這也是無效的,因為它意味著使用者不希望獲取任何資料。開啟分頁合理化後,系統可能會設定一個預設的頁面大小,比如每頁顯示10條資料。
4. 請求的資料大小不合理:
如果使用者請求的資料大小非常大,比如一次請求1000條資料,這可能會給伺服器帶來不必要的壓力。開啟分頁合理化後,系統可能會限制頁面大小的上限,比如最多隻允許每頁顯示100條資料。
二、為啥要設定分頁合理化?
其實上面那些問題對於後端來講很合理,頁碼和頁大小設定不正確查詢不出來值難道不合理嗎?唯一的問題就是如果一次性查詢太多條資料伺服器壓力確實大,但如果是產品要求的那也沒辦法呀!
真正讓我不得不解決這個問題的原因是前端的一個BUG,這個BUG是啥樣的呢?我來給大家描述一下。
1. BUG復現
我們先看看前端的分頁元件
前端的這個分頁元件大家應該很常見,它需要兩個引數:總行數、每頁行數。比如說現在總條數是6條,每頁展示5條,那麼會有2頁,沒啥問題對吧。
那麼,現在我問一個問題:我們切換到第二頁,把第二頁僅剩的一條資料給刪除掉,會出現什麼情況?
理想情況:頁碼自動切換到第1頁,並查詢第一頁的資料;
真實情況:頁碼切換到了第1頁,但是查詢不到資料,這明顯就是一個BUG!
2. BUG分析
1. 使用者切換到第二頁,前端發起了請求,如:http://localhost:8080/user/pageQuery?pageNum=2&pageSize=5 ,此時第2頁有一條資料;
2. 使用者刪除第2頁的唯一資料後,前端發起查詢請求,但還是第2頁的查詢,因為總資料的變化前端只能透過下一次的查詢才能知道,但此時資料查詢為空;
3. 雖然第二次查詢的資料集為空,但是總條數已經變化了,只剩下5條,前端分頁元件根據計算得出只剩下一頁,所以自動切換到第1頁;
可以看出這個BUG是分頁查詢的一個臨界狀態產生的,必現、中低頻,屬於必須修復的那一類。不過這個BUG想甩給前端,估計不行,因為總條數的變化只有後端知道,必須得後端修了。
三、設定分頁合理化
咋一聽這個BUG有點兒複雜,但如果你使用的是PageHelper框架,那麼修復它非常簡單,只需要兩行配置
。
在application.yml或application.properties中新增
pagehelper.helper-dialect=mysql
pagehelper.reasonable=true
只要加了這兩行配置,這個BUG就能解決。因為配置是全域性的,如果你只想對單個查詢場景生效,那就在設定分頁引數的時候,加一個引數,如下:
PageHelper.startPage(pageNumber, pageSize, true);
四、分頁合理化配置的原理說明
這個BUG如果要自己解決的話,是不是感覺有點頭痛了,但是人家PageHelper早就想到這個問題了,就像遊戲開掛一樣,一個配置就解決了這個麻煩的問題。
用的時候確實很爽,但是我卻有點擔心,這個配置現在解決了這個BUG,會不會導致新的BUG呢?如果真的出現了新BUG,我應該怎麼做呢?所以我決定研究一下它的基礎原理。
在com.github.pagehelper.Page類下,找到了這段核心原始碼,這段應該就是分頁合理化的實現邏輯
// 省略其他程式碼
public Page<E> setReasonable(Boolean reasonable) {
if (reasonable == null) {
return this;
}
this.reasonable = reasonable;
//分頁合理化,針對不合理的頁碼自動處理
if (this.reasonable && this.pageNum <= 0) {
this.pageNum = 1;
calculateStartAndEndRow();
}
return this;
}
// 省略其他程式碼
// 省略其他程式碼
/**
* 計算起止行號
*/
private void calculateStartAndEndRow() {
this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;
this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0);
}
// 省略其他程式碼
還有一些程式碼我沒貼,比如PageInterceptor#intercept方法,這裡我整理了一下它的執行流程圖,如下:
看了圖解,這套配置還挺清晰的,懂了怎麼回事兒,用起來也就放心了。記得剛開始寫程式碼時,啥都希望有人給弄好了,最好是拿來即用。但時間一長,自己修過一堆BUG,才發現只有自己弄明白的程式碼才靠譜,什麼都想親手來。等真正搞懂了一些底層的東西,才意識到要想造出好東西,得先學會站在巨人的肩膀上。學習嘛,沒個頭兒!