分頁需求
互聯(lián)網(wǎng)很多業(yè)務(wù)都有分頁拉取數(shù)據(jù)的需求,例如:
(1)微信消息過多時,拉取第N頁消息
(2)京東下單過多時,拉取第N頁訂單
(3)瀏覽58同城,查看第N頁帖子
這些業(yè)務(wù)場景對應(yīng)的消息表,訂單表,帖子表分頁拉取需求有這樣一些特點:
(1)有一個業(yè)務(wù)主鍵id, 例如msg_id, order_id, tiezi_id
(2)分頁排序是按照非業(yè)務(wù)主鍵id來排序的,業(yè)務(wù)中經(jīng)常按照時間time來排序order by
select * from t_tiezi order by time offset 200 limit 100
此處假設(shè)一頁數(shù)據(jù)為100條,均拉取第3頁數(shù)據(jù)。
分庫需求
高并發(fā)大流量的互聯(lián)網(wǎng)架構(gòu),一般通過服務(wù)層來訪問數(shù)據(jù)庫,隨著數(shù)據(jù)量的增大,數(shù)據(jù)庫需要進行水平切分,分庫后將數(shù)據(jù)分布到不同的數(shù)據(jù)庫實例(甚至物理機器)上,以達到降低數(shù)據(jù)量,增加實例數(shù)的擴容目的。
問題的提出
仍然是上述用戶庫的例子,如果業(yè)務(wù)要查詢“最近注冊的第3頁用戶”,該如何實現(xiàn)呢?單庫上,可以
select * from t_user order by time offset 200 limit 100
變成兩個庫后,分庫依據(jù)是uid,排序依據(jù)是time,數(shù)據(jù)庫層失去了time排序的全局視野,數(shù)據(jù)分布在兩個庫上,此時該怎么辦呢?
再總結(jié)一下這個方案的步驟:
(1)將order by time offset X limit Y,改寫成order by time offset 0 limit X+Y
(2)服務(wù)層將改寫后的SQL語句發(fā)往各個分庫:即例子中的各取3頁數(shù)據(jù)
(3)假設(shè)共分為N個庫,服務(wù)層將得到N*(X+Y)條數(shù)據(jù):即例子中的6頁數(shù)據(jù)
(4)服務(wù)層對得到的N*(X+Y)條數(shù)據(jù)進行內(nèi)存排序,內(nèi)存排序后再取偏移量X后的Y條記錄,就是全局視野所需的一頁數(shù)據(jù)
方案缺點(顯而易見):
(1)每個分庫需要返回更多的數(shù)據(jù),增大了網(wǎng)絡(luò)傳輸量(耗網(wǎng)絡(luò));
(2)除了數(shù)據(jù)庫按照time進行排序,服務(wù)層還需要進行二次排序,增大了服務(wù)層的計算量(耗CPU);
(3)最致命的,這個算法隨著頁碼的增大,性能會急劇下降,這是因為SQL改寫后每個分庫要返回X+Y行數(shù)據(jù):返回第3頁,offset中的X=200;假如要返回第100頁,offset中的X=9900,即每個分庫要返回100頁數(shù)據(jù),數(shù)據(jù)量和排序量都將大增,性能平方級下降。
“全局視野法”雖然性能較差,但其業(yè)務(wù)無損,數(shù)據(jù)精準(zhǔn),不失為一種方案,有沒有性能更優(yōu)的方案呢?
業(yè)務(wù)折衷一:禁止跳頁查詢
在數(shù)據(jù)量很大,翻頁數(shù)很多的時候,很多產(chǎn)品并不提供“直接跳到指定頁面”的功能,而只提供“下一頁”的功能,這一個小小的業(yè)務(wù)折衷,就能極大的降低技術(shù)方案的復(fù)雜度。
如上圖,不夠跳頁,那么第一次只能夠查第一頁:
(1)將查詢order by time offset 0 limit 100,改寫成order by time where time>0 limit 100
(2)上述改寫和offset 0 limit 100的效果相同,都是每個分庫返回了一頁數(shù)據(jù)(上圖中粉色部分);
這個上一頁記錄的time_max,會作為第二頁數(shù)據(jù)拉取的查詢條件:
(1)將查詢order by time offset 100 limit 100,改寫成order by time where time>$time_max limit 100
業(yè)務(wù)折衷二:允許數(shù)據(jù)精度損失
“全局視野法”能夠返回業(yè)務(wù)無損的精確數(shù)據(jù),在查詢頁數(shù)較大,例如第100頁時,會有性能問題,此時業(yè)務(wù)上是否能夠接受,返回的100頁不是精準(zhǔn)的數(shù)據(jù),而允許有一些數(shù)據(jù)偏差呢?
數(shù)據(jù)庫分庫-數(shù)據(jù)均衡原理
使用patition key進行分庫,在數(shù)據(jù)量較大,數(shù)據(jù)分布足夠隨機的情況下,各分庫所有非patition key屬性,在各個分庫上的數(shù)據(jù)分布,統(tǒng)計概率情況是一致的。
例如,在uid隨機的情況下,使用uid取模分兩庫,db0和db1:
(1)性別屬性,如果db0庫上的男性用戶占比70%,則db1上男性用戶占比也應(yīng)為70%
(2)年齡屬性,如果db0庫上18-28歲少女用戶比例占比15%,則db1上少女用戶比例也應(yīng)為15%
(3)時間屬性,如果db0庫上每天10:00之前登錄的用戶占比為20%,則db1上應(yīng)該是相同的統(tǒng)計規(guī)律
…
有沒有一種技術(shù)方案,即能夠滿足業(yè)務(wù)的精確需要,無需業(yè)務(wù)折衷,又高性能的方法呢?這就是接下來要介紹的終極武器:“二次查詢法”。
步驟一:查詢改寫
將select * from T order by time offset 1000 limit 5
改寫為select * from T order by time offset 500 limit 5
并投遞給所有的分庫,注意,這個offset的500,來自于全局offset的總偏移量1000,除以水平切分?jǐn)?shù)據(jù)庫個數(shù)2。
如果是3個分庫,則可以改寫為select * from T order by time offset 333 limit 5
假設(shè)這三個分庫返回的數(shù)據(jù)(time, uid)如下:
步驟二:找到所返回3頁全部數(shù)據(jù)的最小值
第一個庫,5條數(shù)據(jù)的time最小值是1487501123
第二個庫,5條數(shù)據(jù)的time最小值是1487501133
第三個庫,5條數(shù)據(jù)的time最小值是1487501143
步驟三:查詢二次改寫
第一次改寫的SQL語句是select * from T order by time offset 333 limit 5
第二次要改寫成一個between語句,between的起點是time_min,between的終點是原來每個分庫各自返回數(shù)據(jù)的最大值:
第一個分庫,第一次返回數(shù)據(jù)的最大值是1487501523
所以查詢改寫為select * from T order by time where time between time_min and 1487501523
第二個分庫,第一次返回數(shù)據(jù)的最大值是1487501323
所以查詢改寫為select * from T order by time where time between time_min and 1487501323
第三個分庫,第一次返回數(shù)據(jù)的最大值是1487501553
所以查詢改寫為select * from T order by time where time between time_min and 1487501553
可以看到:
由于time_min來自原來的分庫一,所以分庫一的返回結(jié)果集和第一次查詢相同(所以其實這次訪問是可以省略的);
分庫二的結(jié)果集,比第一次多返回了1條數(shù)據(jù),頭部的1條記錄(time最小的記錄)是新的(上圖中粉色記錄);
分庫三的結(jié)果集,比第一次多返回了2條數(shù)據(jù),頭部的2條記錄(time最小的2條記錄)是新的(上圖中粉色記錄);
在第一個庫中,time_min在第一個庫的offset是333
在第二個庫中,(1487501133, uid_aa)的offset是333(根據(jù)第一次查詢條件得出的),故虛擬time_min在第二個庫的offset是331
在第三個庫中,(1487501143, uid_aaa)的offset是333(根據(jù)第一次查詢條件得出的),故虛擬time_min在第三個庫的offset是330
綜上,time_min在全局的offset是333+331+330=994
步驟五:既然得到了time_min在全局的offset,就相當(dāng)于有了全局視野,根據(jù)第二次的結(jié)果集,就能夠得到全局offset 1000 limit 5的記錄
第二次查詢在各個分庫返回的結(jié)果集是有序的,又知道了time_min在全局的offset是994,一路排下來,容易知道全局offset 1000 limit 5的一頁記錄(上圖中黃色記錄)。
是不是非常巧妙?這種方法的優(yōu)點是:可以精確的返回業(yè)務(wù)所需數(shù)據(jù),每次返回的數(shù)據(jù)量都非常小,不會隨著翻頁增加數(shù)據(jù)的返回量。
不足是:需要進行兩次數(shù)據(jù)庫查詢。
今天介紹了解決“跨N庫分頁”這一難題的四種方法:
方法一:全局視野法
(1)將order by time offset X limit Y,改寫成order by time offset 0 limit X+Y
(2)服務(wù)層對得到的N*(X+Y)條數(shù)據(jù)進行內(nèi)存排序,內(nèi)存排序后再取偏移量X后的Y條記錄
這種方法隨著翻頁的進行,性能越來越低。
方法二:業(yè)務(wù)折衷法-禁止跳頁查詢
(1)用正常的方法取得第一頁數(shù)據(jù),并得到第一頁記錄的time_max
(2)每次翻頁,將order by time offset X limit Y,改寫成order by time where time>$time_max limit Y
以保證每次只返回一頁數(shù)據(jù),性能為常量。
方法三:業(yè)務(wù)折衷法-允許模糊數(shù)據(jù)
(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)設(shè)置虛擬time_min,找到time_min在各個分庫的offset,從而得到time_min在全局的offset
(5)得到了time_min在全局的offset,自然得到了全局的offset X limit Y
更多建議: