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