(轉)業界難題-「跨庫分頁」的四種方案

1、需求緣起算法

分頁需求數據庫

互聯網不少業務都有分頁拉取數據的需求,例如:服務器

(1)微信消息過多時,拉取第N頁消息微信

(2)京東下單過多時,拉取第N頁訂單網絡

(3)瀏覽58同城,查看第N頁帖子架構

 

這些業務場景對應的消息表,訂單表,帖子表分頁拉取需求有這樣一些特色:併發

(1)有一個業務主鍵id, 例如msg_id, order_id, tiezi_id負載均衡

(2)分頁排序是按照非業務主鍵id來排序的,業務中常常按照時間time來排序order by高併發

 

在數據量不大時,能夠經過在排序字段time上創建索引,利用SQL提供的offset/limit功能就能知足分頁查詢需求性能

select * from t_msg order by time offset 200 limit 100

select * from t_order order by time offset 200 limit 100

select * from t_tiezi order by time offset 200 limit 100

此處假設一頁數據爲100條,均拉取第3頁數據。

 

分庫需求

高併發大流量的互聯網架構,通常經過服務層來訪問數據庫,隨着數據量的增大,數據庫須要進行水平切分,分庫後將數據分佈到不一樣的數據庫實例(甚至物理機器)上,以達到下降數據量,增長實例數的擴容目的。

 

一旦涉及分庫,逃不開「分庫依據」patition key的概念,使用哪個字段來水平切分數據庫呢:大部分的業務場景,會使用業務主鍵id。

 

肯定了分庫依據patition key後,接下來要肯定的是分庫算法:大部分的業務場景,會使用業務主鍵id取模的算法來分庫,這樣即可以保證每一個庫的數據分佈是均勻的,又可以保證每一個庫的請求分佈是均勻的,實在是簡單實現負載均衡的好方法,此法在互聯網架構中應用頗多。

 

舉一個更具體的例子:

用戶庫user,水平切分後變爲兩個庫,分庫依據patition key是uid,分庫算法是uid取模:uid%2餘0的數據會落到db0,uid%2餘1的數據會落到db1。

 

問題的提出

仍然是上述用戶庫的例子,若是業務要查詢「最近註冊的第3頁用戶」,該如何實現呢?單庫上,能夠

select * from t_user order by time offset 200 limit 100

變成兩個庫後,分庫依據是uid,排序依據是time,數據庫層失去了time排序的全局視野,數據分佈在兩個庫上,此時該怎麼辦呢?

 

如何知足「跨越多個水平切分數據庫,且分庫依據與排序依據爲不一樣屬性,並須要進行分頁」的查詢需求,實現 select * from T order by time offset X limit Y的跨庫分頁SQL,是本文將要討論的技術問題。

 

2、全局視野法

如上圖所述,服務層經過uid取模將數據分佈到兩個庫上去以後,每一個數據庫都失去了全局視野,數據按照time局部排序以後,無論哪一個分庫的第3頁數據,都不必定是全局排序的第3頁數據。

 

那到底哪些數據纔是全局排序的第3頁數據呢,暫且分三種狀況討論。

 

(1)極端狀況,兩個庫的數據徹底同樣

若是兩個庫的數據徹底相同,只須要每一個庫offset一半,再取半頁,就是最終想要的數據(如上圖中粉色部分數據)。

 

(2)極端狀況,結果數據來自一個庫

也可能兩個庫的數據分佈及其不均衡,例如db0的全部數據的time都大於db1的全部數據的time,則可能出現:一個庫的第3頁數據,就是全局排序後的第3頁數據(如上圖中粉色部分數據)。

 

(3)通常狀況,每一個庫數據各包含一部分

正常狀況下,全局排序的第3頁數據,每一個庫都會包含一部分(如上圖中粉色部分數據)。

 

因爲不清楚究竟是哪一種狀況,因此必須每一個庫都返回3頁數據,所獲得的6頁數據在服務層進行內存排序,獲得數據全局視野,再取第3頁數據,便可以獲得想要的全局分頁數據。

 

再總結一下這個方案的步驟:

(1)將order by time offset X limit Y,改寫成order by time offset 0 limit X+Y

(2)服務層將改寫後的SQL語句發往各個分庫:即例子中的各取3頁數據

(3)假設共分爲N個庫,服務層將獲得N*(X+Y)條數據:即例子中的6頁數據

(4)服務層對獲得的N*(X+Y)條數據進行內存排序,內存排序後再取偏移量X後的Y條記錄,就是全局視野所需的一頁數據

 

方案優勢:經過服務層修改SQL語句,擴大數據召回量,可以獲得全局視野,業務無損,精準返回所需數據。

 

方案缺點(顯而易見):

(1)每一個分庫須要返回更多的數據,增大了網絡傳輸量(耗網絡);

(2)除了數據庫按照time進行排序,服務層還須要進行二次排序,增大了服務層的計算量(耗CPU);

(3)最致命的,這個算法隨着頁碼的增大,性能會急劇降低,這是由於SQL改寫後每一個分庫要返回X+Y行數據:返回第3頁,offset中的X=200;假如要返回第100頁,offset中的X=9900,即每一個分庫要返回100頁數據,數據量和排序量都將大增,性能平方級降低。

 

3、業務折衷法

「全局視野法」雖然性能較差,但其業務無損,數據精準,不失爲一種方案,有沒有性能更優的方案呢?

 

任何脫離業務的架構設計都是耍流氓」,技術方案須要折衷,在技術難度較大的狀況下,業務需求的折衷可以極大的簡化技術方案。

 

業務折衷一:禁止跳頁查詢

在數據量很大,翻頁數不少的時候,不少產品並不提供「直接跳到指定頁面」的功能,而只提供「下一頁」的功能,這一個小小的業務折衷,就能極大的下降技術方案的複雜度。

如上圖,不夠跳頁,那麼第一次只可以查第一頁:

(1)將查詢order by time offset 0 limit 100,改寫成order by time where time>0 limit 100

(2)上述改寫和offset 0 limit 100的效果相同,都是每一個分庫返回了一頁數據(上圖中粉色部分);

(3)服務層獲得2頁數據,內存排序,取出前100條數據,做爲最終的第一頁數據,這個全局的第一頁數據,通常來講每一個分庫都包含一部分數據(如上圖粉色部分);

 

咦,這個方案也須要服務器內存排序,豈不是和「全局視野法」同樣麼?第一頁數據的拉取確實同樣,但每一次「下一頁」拉取的方案就不同了。

 

點擊「下一頁」時,須要拉取第二頁數據,在第一頁數據的基礎之上,可以找到第一頁數據time的最大值:

這個上一頁記錄的time_max,會做爲第二頁數據拉取的查詢條件:

(1)將查詢order by time offset 100 limit 100,改寫成order by time where time>$time_max limit 100

(2)這下不是返回2頁數據了(「全局視野法,會改寫成offset 0 limit 200」),每一個分庫仍是返回一頁數據(如上圖中粉色部分);

(3)服務層獲得2頁數據,內存排序,取出前100條數據,做爲最終的第2頁數據,這個全局的第2頁數據,通常來講也是每一個分庫都包含一部分數據(如上圖粉色部分);

 

如此往復,查詢全局視野第100頁數據時,不是將查詢條件改寫爲offset 0 limit 9900+100(返回100頁數據),而是改寫爲time>$time_max99 limit 100(仍返回一頁數據),以保證數據的傳輸量和排序的數據量不會隨着不斷翻頁而致使性能降低。

 

業務折衷二:容許數據精度損失

「全局視野法」可以返回業務無損的精確數據,在查詢頁數較大,例如第100頁時,會有性能問題,此時業務上是否可以接受,返回的100頁不是精準的數據,而容許有一些數據誤差呢?

 

數據庫分庫-數據均衡原理

使用patition key進行分庫,在數據量較大,數據分佈足夠隨機的狀況下,各分庫全部非patition key屬性,在各個分庫上的數據分佈,統計機率狀況是一致的。

 

例如,在uid隨機的狀況下,使用uid取模分兩庫,db0和db1:

(1)性別屬性,若是db0庫上的男性用戶佔比70%,則db1上男性用戶佔比也應爲70%

(2)年齡屬性,若是db0庫上18-28歲少女用戶比例佔比15%,則db1上少女用戶比例也應爲15%

(3)時間屬性,若是db0庫上天天10:00以前登陸的用戶佔比爲20%,則db1上應該是相同的統計規律

 

利用這一原理,要查詢全局100頁數據,offset 9900 limit 100改寫爲offset 4950 limit 50,每一個分庫偏移4950(一半),獲取50條數據(半頁),獲得的數據集的並集,基本可以認爲,是全局數據的offset 9900 limit 100的數據,固然,這一頁數據的精度,並非精準的。

 

根據實際業務經驗,用戶都要查詢第100頁網頁、帖子、郵件的數據了,這一頁數據的精準性損失,業務上每每是能夠接受的,但此時技術方案的複雜度便大大下降了,既不須要返回更多的數據,也不須要進行服務內存排序了。

 

4、終極武器-二次查詢法

有沒有一種技術方案,即可以知足業務的精確須要,無需業務折衷,又高性能的方法呢?這就是接下來要介紹的終極武器:「二次查詢法」。

 

爲了方便舉例,假設一頁只有5條數據,查詢第200頁的SQL語句爲select * from T order by time offset 1000 limit 5;

 

步驟一:查詢改寫

將select * from T order by time offset 1000 limit 5

改寫爲select * from T order by time offset 500 limit 5

並投遞給全部的分庫,注意,這個offset的500,來自於全局offset的總偏移量1000,除以水平切分數據庫個數2。

 

若是是3個分庫,則能夠改寫爲select * from T order by time offset 333 limit 5

假設這三個分庫返回的數據(time, uid)以下:

能夠看到,每一個分庫都是返回的按照time排序的一頁數據。

 

步驟二:找到所返回3頁所有數據的最小值

第一個庫,5條數據的time最小值是1487501123

第二個庫,5條數據的time最小值是1487501133

第三個庫,5條數據的time最小值是1487501143

故,三頁數據中,time最小值來自第一個庫,time_min=1487501123,這個過程只須要比較各個分庫第一條數據,時間複雜度很低

 

步驟三:查詢二次改寫

第一次改寫的SQL語句是select * from T order by time offset 333 limit 5

第二次要改寫成一個between語句,between的起點是time_min,between的終點是原來每一個分庫各自返回數據的最大值:

第一個分庫,第一次返回數據的最大值是1487501523

因此查詢改寫爲select * from T order by time where time between time_min and 1487501523

 

第二個分庫,第一次返回數據的最大值是1487501323

因此查詢改寫爲select * from T order by time where time between time_min and 1487501323

 

第三個分庫,第一次返回數據的最大值是1487501553

因此查詢改寫爲select * from T order by time where time between time_min and 1487501553

 

相對第一次查詢,第二次查詢條件放寬了,故第二次查詢會返回比第一次查詢結果集更多的數據,假設這三個分庫返回的數據(time, uid)以下:

能夠看到:

因爲time_min來自原來的分庫一,因此分庫一的返回結果集和第一次查詢相同(因此其實此次訪問是能夠省略的);

分庫二的結果集,比第一次多返回了1條數據,頭部的1條記錄(time最小的記錄)是新的(上圖中粉色記錄);

分庫三的結果集,比第一次多返回了2條數據,頭部的2條記錄(time最小的2條記錄)是新的(上圖中粉色記錄);

 

步驟四:在每一個結果集中虛擬一個time_min記錄,找到time_min在全局的offset

在第一個庫中,time_min在第一個庫的offset是333

在第二個庫中,(1487501133, uid_aa)的offset是333(根據第一次查詢條件得出的),故虛擬time_min在第二個庫的offset是331

在第三個庫中,(1487501143, uid_aaa)的offset是333(根據第一次查詢條件得出的),故虛擬time_min在第三個庫的offset是330

 

綜上,time_min在全局的offset是333+331+330=994

 

步驟五:既然獲得了time_min在全局的offset,就至關於有了全局視野,根據第二次的結果集,就可以獲得全局offset 1000 limit 5的記錄

第二次查詢在各個分庫返回的結果集是有序的,又知道了time_min在全局的offset是994,一路排下來,容易知道全局offset 1000 limit 5的一頁記錄(上圖中黃色記錄)。

 

是否是很是巧妙?這種方法的優勢是:能夠精確的返回業務所需數據,每次返回的數據量都很是小,不會隨着翻頁增長數據的返回量。

 

不足是:須要進行兩次數據庫查詢。

 

5、總結

今天介紹瞭解決「跨N庫分頁」這一難題的四種方法:

 

方法一:全局視野法

(1)將order by time offset X limit Y,改寫成order by time offset 0 limit X+Y

(2)服務層對獲得的N*(X+Y)條數據進行內存排序,內存排序後再取偏移量X後的Y條記錄

這種方法隨着翻頁的進行,性能愈來愈低。

 

方法二:業務折衷法-禁止跳頁查詢

(1)用正常的方法取得第一頁數據,並獲得第一頁記錄的time_max

(2)每次翻頁,將order by time offset X limit Y,改寫成order by time where time>$time_max limit Y

以保證每次只返回一頁數據,性能爲常量。

 

方法三:業務折衷法-容許模糊數據

(1)將order by time offset X limit Y,改寫成order by time offset X/N limit Y/N

 

方法四:二次查詢法

(1)將order by time offset X limit Y,改寫成order by time offset X/N limit Y

(2)找到最小值time_min

(3)between二次查詢,order by time between $time_min and $time_i_max

(4)設置虛擬time_min,找到time_min在各個分庫的offset,從而獲得time_min在全局的offset

(5)獲得了time_min在全局的offset,天然獲得了全局的offset X limit Y

相關文章
相關標籤/搜索