上億資料怎麼玩深度分頁?相容MySQL + ES + MongoDB
阿新 • • 發佈:2020-07-16
## 面試題 & 真實經歷
> ***面試題:***在資料量很大的情況下,怎麼實現深度分頁?
大家在面試時,或者準備面試中可能會遇到上述的問題,大多的回答基本上是`分庫分表建索引`,這是一種很`標準的正確回答`,但現實總是很骨感,所以面試官一般會追問你一句,現在工期不足,人員不足,該怎麼實現深度分頁?
這個時候沒有實際經驗的同學基本麻爪,So,請聽我娓娓道來。
## 慘痛的教訓
***首先必須明確一點:***深度分頁可以做,但是深度隨機跳頁絕對需要禁止。
上一張圖:
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC83LzE1LzE3MzRlNDA4MzAxMzA5NWY?x-oss-process=image/format,png)
你們猜,我點一下第`142360`頁,服務會不會爆炸?
像`MySQL`,`MongoDB`資料庫還好,本身就是專業的資料庫,處理的不好,最多就是慢,但如果涉及到`ES`,性質就不一樣了,我們不得不利用 `SearchAfter` Api,去迴圈獲取資料,這就牽扯到記憶體佔用的問題,如果當時程式碼寫的不優雅,直接就可能導致記憶體溢位。
## 為什麼不能允許隨機深度跳頁
從技術的角度淺顯的聊一聊為什麼不能允許隨機深度跳頁,或者說為什麼不建議深度分頁
### MySQL
分頁的基本原理:
```mysql
SELECT * FROM test ORDER BY id DESC LIMIT 10000, 20;
```
LIMIT 10000 , 20的意思掃描滿足條件的10020行,扔掉前面的10000行,返回最後的20行。如果是LIMIT 1000000 , 100,需要掃描1000100 行,在一個高併發的應用裡,每次查詢需要掃描超過100W行,不炸才怪。
### MongoDB
分頁的基本原理:
```mysql
db.t_data.find().limit(5).skip(5);
```
同樣的,隨著頁碼的增大,skip 跳過的條目也會隨之變大,而這個操作是通過 cursor 的迭代器來實現的,對於cpu的消耗會非常明顯,當頁碼非常大時且頻繁時,必然爆炸。
### ElasticSearch
從業務的角度來說,`ElasticSearch`不是典型的資料庫,它是一個搜尋引擎,如果在篩選條件下沒有搜尋出想要的資料,繼續深度分頁也不會找到想要的資料,退一步講,假如我們把`ES`作為資料庫來使用進行查詢,在進行分頁的時候一定會遇到`max_result_window `的限制,看到沒,官方都告訴你最大偏移量限制是一萬。
查詢流程:
1. 如查詢第501頁,每頁10條,客戶端傳送請求到某節點
2. 此節點將資料廣播到各個分片,各分片各自查詢前 5010 條資料
3. 查詢結果返回至該節點,然後對資料進行整合,取出前 5010 條資料
4. 返回給客戶端
由此可以看出為什麼要限制偏移量,另外,如果使用 `Search After` 這種滾動式API進行深度跳頁查詢,也是一樣需要每次滾動幾千條,可能一共需要滾動上百萬,千萬條資料,就為了最後的20條資料,效率可想而知。
## 再次和產品對線
俗話說的好,技術解決不了的問題,就由業務來解決!
在實習的時候信了產品的邪,必須實現深度分頁 + 跳頁,如今必須`撥亂反正`,業務上必須有如下更改:
- 儘可能的增加預設的篩選條件,如:時間週期,目的是為了減少資料量的展示
- 修改跳頁的展現方式,改為滾動顯示,或小範圍跳頁
滾動顯示參考圖:
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC83LzE1LzE3MzRlNDBjNWIyMTNmMTc?x-oss-process=image/format,png)
小規模跳頁參考圖:
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC83LzE1LzE3MzRlNDBlY2M2MzQ1NzA?x-oss-process=image/format,png)
## 通用解決方案
短時間內快速解決的方案主要是以下幾點:
- 必備:對排序欄位,篩選條件務必設定好索引
- 核心:利用小範圍頁碼的已知資料,或者滾動載入的已知資料,減少偏移量
- 額外:如果遇到不好處理的情況,也可以獲取多餘的資料,進行一定的擷取,效能影響並不大
### MySQL
原分頁SQL:
```mysql
# 第一頁
SELECT * FROM `year_score` where `year` = 2017 ORDER BY id limit 0, 20;
# 第N頁
SELECT * FROM `year_score` where `year` = 2017 ORDER BY id limit (N - 1) * 20, 20;
```
通過上下文關係,改寫為:
```mysql
# XXXX 代表已知的資料
SELECT * FROM `year_score` where `year` = 2017 and id > XXXX ORDER BY id limit 20;
```
在 [沒內鬼,來點乾貨!SQL優化和診斷](https://juejin.im/post/5ea16dede51d45470b4ffc5b#heading-9) 一文中提到過,LIMIT會在滿足條件下停止查詢,因此該方案的掃描總量會急劇減少,效率提升Max!
### ES
方案和`MySQL`相同,此時我們就可以隨用所欲的使用 `FROM-TO` Api,而且不用考慮最大限制的問題。
### MongoDB
方案基本類似,基本程式碼如下:
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC83LzE1LzE3MzRlNDExOWVhNjcxMDU?x-oss-process=image/format,png)
相關效能測試:
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC83LzE1LzE3MzRlNDEzOGUwMTQ2YjE?x-oss-process=image/format,png)
## 如果非要深度隨機跳頁
如果你沒有槓過產品經理,又該怎麼辦呢,沒關係,還有一絲絲的機會。
在 [SQL優化](https://juejin.im/post/5ea16dede51d45470b4ffc5b#heading-8) 一文中還提到過`MySQL`深度分頁的處理技巧,程式碼如下:
```mysql
# 反例(耗時129.570s)
select * from task_result LIMIT 20000000, 10;
# 正例(耗時5.114s)
SELECT a.* FROM task_result a, (select id from task_result LIMIT 20000000, 10) b where a.id = b.id;
# 說明
# task_result表為生產環境的一個表,總資料量為3400萬,id為主鍵,偏移量達到2000萬
```
該方案的核心邏輯即基於`聚簇索引`,在不通過`回表`的情況下,快速拿到指定偏移量資料的主鍵ID,然後利用`聚簇索引`進行回表查詢,此時總量僅為10條,效率很高。
因此我們在處理`MySQL`,`ES`,`MongoDB`時,也可以採用一樣的辦法:
1. 限制獲取的欄位,只通過篩選條件,深度分頁獲取主鍵ID
2. 通過主鍵ID定向查詢需要的資料
瑕疵:當偏移量非常大時,耗時較長,如文中的 5s
## 最後
參考文章:[MongoDB中文社群](https://mongoing.com/archives/25469)
感謝 @程大設計師 為我傾情設計的二維碼