深度分頁,我都是這麼玩的

架構擺渡人發表於2022-02-20

大家好,我是架構擺渡人。這是實踐經驗系列的第十一篇文章,這個系列會給大家分享很多在實際工作中有用的經驗,如果有收穫,還請分享給更多的朋友。

分頁查詢,無論是在B端的系統,還是C端的應用,都有著廣泛的應用。只不過是應用方式和對效能的要求不一樣而已。

在B端的系統中一般都是一個列表,下面有一個分頁的元件,可以選擇第幾頁的資料,可以進行上下分頁,這種就是最常見的分頁方式,對應到資料庫中我們常實現的方式就是limit 0,10這種。

在C端的應用中,也有分頁查詢的場景,但是對應效能要求比較高,我們都知道傳統limit在頁數越大的時候,效能也越差,主要是跳過的資料越多,回表的次數也多,這些時間都浪費了。所以一般都不會在C端應用中使用傳統的分頁方式。

其次C端應用的分頁都是沒有分頁元件的,以訂單列表來說,是個分頁查詢的場景,在APP中是滑動下拉載入分頁。

為了提高效能,一般會採用ID直接定位的方式來做分頁,改寫SQL如下:

select * from table where id < #{lastId} order by id desc limit #{limit}

改寫之後,就能根據上次返回的ID直接通過聚簇索引定位,然後取出對應的條數即可。

這樣改完之後,無論使用者滑到多少頁,效能都是很快的。但是這種方式也會存在一個問題,就是你的主鍵ID必須是自增有序才行。可能有同學會問:還有無序的主鍵ID嗎?

肯定是有的,假設你們業務發展很快,需要考慮整個機房不能提供服務的場景,這個時候就需要做異地多活了。

在多活場景下,如果是單元庫,會進行雙向複製,此時主鍵ID如果都是自增的就會存在衝突問題。當然可以通過設定不同機房不同的自增步長來解決,這種方式不太靈活,當後面擴機房的時候又需要調整。

另一種方式就是接入分散式ID,分散式ID一般的解決方案有snowflake,segment等。但在分散式場景下要提供完全遞增有序的很難,所以上面的分頁也會存在一定的問題。比如訂單列表,使用者下完單後去列表檢視,很有可能最新的訂單不在第一條,因為你的ID不是全域性遞增。

這樣的問題我們如何解決呢?大家想想,在現實生活中什麼是遞增的呢?答案就是時間

所以,我們可以在表中單獨加個時間欄位來保證有序性。這個時間的精度一定要高,比如微妙,納秒級別,這樣的高精度才能防止重複。還得建一個唯一索引來保證唯一性,確保萬無一失。

有了這個時間欄位,程式就不要去賦值了,直接使用資料庫的預設值,當然批量插入需要注意,因為批量插入的時間會一樣,所以程式中要禁止批量插入。

假設時間欄位有重複的,會對分頁造成影響嗎?肯定有影響的,我們舉個例子看下就知道了。

4     2022-01-01 12:12:12.111431
3     2022-01-01 12:12:12.111431
2     2022-01-01 12:12:10.111431
1     2022-01-01 12:12:09.111431

我們的SQL如下:

select * from table order by time desc limit 1

那麼第一頁的時候是沒有lastTime值的,所以在拼接SQL的地方要做判斷。第一頁查出的資料是ID為1,時間為2022-01-01 12:12:12.111431的資料。

第二頁的SQL如下:

select * from table where time < '2022-01-01 12:12:12.111431' order by time desc limit 1

獲取的結果是ID為3,時間為2022-01-01 12:12:10.111431的資料,你會發現ID為2的資料丟失了,因為它的時間跟第一條一模一樣,這就是問題所在,所以我們要保證時間欄位的唯一性。

如果非得要通過SQL解決也是可以的,我們可以將查詢的SQL改寫下,如下:

select * from table where time < '2022-01-01 12:12:12.111431' or (time='2022-01-01 12:12:12.111431' and id < 4) order by time desc,id desc limit 1

通過加入or條件匹配最後一個時間,如果時間又相同的就會符合條件,並且這條資料的時間是小於之前最後一條資料的ID, 這樣就可以把重複的資料查出來了。

需要注意的是之前我們返回給客戶端只需要最後一條資料的ID, 那麼現在就要返回ID+時間了。

相關文章