www.久久久久|狼友网站av天堂|精品国产无码a片|一级av色欲av|91在线播放视频|亚洲无码主播在线|国产精品草久在线|明星AV网站在线|污污内射久久一区|婷婷综合视频网站

當(dāng)前位置:首頁(yè) > 公眾號(hào)精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]作者:vivo平臺(tái)產(chǎn)品開(kāi)發(fā)團(tuán)隊(duì)-HanLei一、背景提起分庫(kù)分表,對(duì)于大部分服務(wù)器開(kāi)發(fā)來(lái)說(shuō),其實(shí)并不是一個(gè)新鮮的名詞。隨著業(yè)務(wù)的發(fā)展,我們表中的數(shù)據(jù)量會(huì)變的越來(lái)越大,字段也可能隨著業(yè)務(wù)復(fù)雜度的升高而逐漸增多,我們?yōu)榱私鉀Q單表的查詢性能問(wèn)題,一般會(huì)進(jìn)行分表操作。同時(shí)我們業(yè)務(wù)的用戶活...

作者:vivo平臺(tái)產(chǎn)品開(kāi)發(fā)團(tuán)隊(duì)-Han Lei

一、背景


提起分庫(kù)分表,對(duì)于大部分服務(wù)器開(kāi)發(fā)來(lái)說(shuō),其實(shí)并不是一個(gè)新鮮的名詞。隨著業(yè)務(wù)的發(fā)展,我們表中的數(shù)據(jù)量會(huì)變的越來(lái)越大,字段也可能隨著業(yè)務(wù)復(fù)雜度的升高而逐漸增多,我們?yōu)榱私鉀Q單表的查詢性能問(wèn)題,一般會(huì)進(jìn)行分表操作。


同時(shí)我們業(yè)務(wù)的用戶活躍度也會(huì)越來(lái)越高,并發(fā)量級(jí)不斷加大,那么可能會(huì)達(dá)到單個(gè)數(shù)據(jù)庫(kù)的處理能力上限。此時(shí)我們?yōu)榱私鉀Q數(shù)據(jù)庫(kù)的處理性能瓶頸,一般會(huì)進(jìn)行分庫(kù)操作。不管是分庫(kù)操作還是分表操作,我們一般都有兩種方式應(yīng)對(duì),一種是垂直拆分,一種是水平拆分。


關(guān)于兩種拆分方式的區(qū)別和特點(diǎn),互聯(lián)網(wǎng)上參考資料眾多,很多人都寫(xiě)過(guò)相關(guān)內(nèi)容,這里就不再進(jìn)行詳細(xì)贅述,有興趣的讀者可以自行檢索。


此文主要詳細(xì)聊一聊,我們最實(shí)用最常見(jiàn)的水平分庫(kù)分表方式中的一些特殊細(xì)節(jié),希望能幫助大家避免走彎路,找到最合適自身業(yè)務(wù)的分庫(kù)分表設(shè)計(jì)。

【注1】本文中的案例均基于Mysql數(shù)據(jù)庫(kù),下文中的分庫(kù)分表統(tǒng)指水平分庫(kù)分表。


【注2】后文中提到到M庫(kù)N表,均指共M個(gè)數(shù)據(jù)庫(kù),每個(gè)數(shù)據(jù)庫(kù)共N個(gè)分表,即總表個(gè)數(shù)其實(shí)為M*N。


二、什么是一個(gè)好的分庫(kù)分表方案?


2.1 方案可持續(xù)性


前期業(yè)務(wù)數(shù)據(jù)量級(jí)不大,流量較低的時(shí)候,我們無(wú)需分庫(kù)分表,也不建議分庫(kù)分表。但是一旦我們要對(duì)業(yè)務(wù)進(jìn)行分庫(kù)分表設(shè)計(jì)時(shí),就一定要考慮到分庫(kù)分表方案的可持續(xù)性。


那何為可持續(xù)性?其實(shí)就是:業(yè)務(wù)數(shù)據(jù)量級(jí)和業(yè)務(wù)流量未來(lái)進(jìn)一步升高達(dá)到新的量級(jí)的時(shí)候,我們的分庫(kù)分表方案可以持續(xù)使用。


一個(gè)通俗的案例,假定當(dāng)前我們分庫(kù)分表的方案為10庫(kù)100表,那么未來(lái)某個(gè)時(shí)間點(diǎn),若10個(gè)庫(kù)仍然無(wú)法應(yīng)對(duì)用戶的流量壓力,或者10個(gè)庫(kù)的磁盤使用即將達(dá)到物理上限時(shí),我們的方案能夠進(jìn)行平滑擴(kuò)容。


在后文中我們將介紹下目前業(yè)界常用的翻倍擴(kuò)容法和一致性Hash擴(kuò)容法。


2.2 數(shù)據(jù)偏斜問(wèn)題


一個(gè)良好的分庫(kù)分表方案,它的數(shù)據(jù)應(yīng)該是需要比較均勻的分散在各個(gè)庫(kù)表中的。如果我們進(jìn)行一個(gè)拍腦袋式的分庫(kù)分表設(shè)計(jì),很容易會(huì)遇到以下類似問(wèn)題:

a、某個(gè)數(shù)據(jù)庫(kù)實(shí)例中,部分表的數(shù)據(jù)很多,而其他表中的數(shù)據(jù)卻寥寥無(wú)幾,業(yè)務(wù)上的表現(xiàn)經(jīng)常是延遲忽高忽低,飄忽不定。


b、數(shù)據(jù)庫(kù)集群中,部分集群的磁盤使用增長(zhǎng)特別塊,而部分集群的磁盤增長(zhǎng)卻很緩慢。每個(gè)庫(kù)的增長(zhǎng)步調(diào)不一致,這種情況會(huì)給后續(xù)的擴(kuò)容帶來(lái)步調(diào)不一致,無(wú)法統(tǒng)一操作的問(wèn)題。


這邊我們定義分庫(kù)分表最大數(shù)據(jù)偏斜率為 :(數(shù)據(jù)量最大樣本 - 數(shù)據(jù)量最小樣本)/ 數(shù)據(jù)量最小樣本。一般來(lái)說(shuō),如果我們的最大數(shù)據(jù)偏斜率在5%以內(nèi)是可以接受的。


你分庫(kù)分表的姿勢(shì)對(duì)么?——詳談水平分庫(kù)分表


三、常見(jiàn)的分庫(kù)分表方案


3.1 Range分庫(kù)分表


顧名思義,該方案根據(jù)數(shù)據(jù)范圍劃分?jǐn)?shù)據(jù)的存放位置。


舉個(gè)最簡(jiǎn)單例子,我們可以把訂單表按照年份為單位,每年的數(shù)據(jù)存放在單獨(dú)的庫(kù)(或者表)中。如下圖所示:

/** * 通過(guò)年份分表 * * @param orderId * @return */public static String rangeShardByYear(String orderId) { int year = Integer.parseInt(orderId.substring(0, 4)); return "t_order_" year;}

通過(guò)數(shù)據(jù)的范圍進(jìn)行分庫(kù)分表,該方案是最樸實(shí)的一種分庫(kù)方案,它也可以和其他分庫(kù)分表方案靈活結(jié)合使用。時(shí)下非常流行的分布式數(shù)據(jù)庫(kù):TiDB數(shù)據(jù)庫(kù),針對(duì)TiKV中數(shù)據(jù)的打散,也是基于Range的方式進(jìn)行,將不同范圍內(nèi)的[StartKey,EndKey)分配到不同的Region上。


下面我們看看該方案的缺點(diǎn):


  • a、最明顯的就是數(shù)據(jù)熱點(diǎn)問(wèn)題,例如上面案例中的訂單表,很明顯當(dāng)前年度所在的庫(kù)表屬于熱點(diǎn)數(shù)據(jù),需要承載大部分的IO和計(jì)算資源。


  • b、新庫(kù)和新表的追加問(wèn)題。一般我們線上運(yùn)行的應(yīng)用程序是沒(méi)有數(shù)據(jù)庫(kù)的建庫(kù)建表權(quán)限的,故我們需要提前將新的庫(kù)表提前建立,防止線上故障。

這點(diǎn)非常容易被遺忘,尤其是穩(wěn)定跑了幾年沒(méi)有迭代任務(wù),或者人員又交替頻繁的模塊。

  • c、業(yè)務(wù)上的交叉范圍內(nèi)數(shù)據(jù)的處理。舉個(gè)例子,訂單模塊無(wú)法避免一些中間狀態(tài)的數(shù)據(jù)補(bǔ)償邏輯,即需要通過(guò)定時(shí)任務(wù)到訂單表中掃描那些長(zhǎng)時(shí)間處于待支付確認(rèn)等狀態(tài)的訂單。


這里就需要注意了,因?yàn)槭峭ㄟ^(guò)年份進(jìn)行分庫(kù)分表,那么元旦的那一天,你的定時(shí)任務(wù)很有可能會(huì)漏掉上一年的最后一天的數(shù)據(jù)掃描。


3.2 Hash分庫(kù)分表


雖然分庫(kù)分表的方案眾多,但是Hash分庫(kù)分表是最大眾最普遍的方案,也是本文花最大篇幅描述的部分。


針對(duì)Hash分庫(kù)分表的細(xì)節(jié)部分,相關(guān)的資料并不多。大部分都是闡述一下概念舉幾個(gè)示例,而細(xì)節(jié)部分并沒(méi)有特別多的深入,如果未結(jié)合自身業(yè)務(wù)貿(mào)然參考引用,后期非常容易出現(xiàn)各種問(wèn)題。


在正式介紹這種分庫(kù)分表方式之前,我們先看幾個(gè)常見(jiàn)的錯(cuò)誤案例。


常見(jiàn)錯(cuò)誤案例一:非互質(zhì)關(guān)系導(dǎo)致的數(shù)據(jù)偏斜問(wèn)題

public static ShardCfg shard(String userId) { int hash = userId.hashCode(); // 對(duì)庫(kù)數(shù)量取余結(jié)果為庫(kù)序號(hào) int dbIdx = Math.abs(hash % DB_CNT); // 對(duì)表數(shù)量取余結(jié)果為表序號(hào) int tblIdx = Math.abs(hash % TBL_CNT); return new ShardCfg(dbIdx, tblIdx);}

上述方案是初次使用者特別容易進(jìn)入的誤區(qū),用Hash值分別對(duì)分庫(kù)數(shù)和分表數(shù)取余,得到庫(kù)序號(hào)和表序號(hào)。其實(shí)稍微思索一下,我們就會(huì)發(fā)現(xiàn),以10庫(kù)100表為例,如果一個(gè)Hash值對(duì)100取余為0,那么它對(duì)10取余也必然為0。


這就意味著只有0庫(kù)里面的0表才可能有數(shù)據(jù),而其他庫(kù)中的0表永遠(yuǎn)為空!


類似的我們還能推導(dǎo)到,0庫(kù)里面的共100張表,只有10張表中(個(gè)位數(shù)為0的表序號(hào))才可能有數(shù)據(jù)。這就帶來(lái)了非常嚴(yán)重的數(shù)據(jù)偏斜問(wèn)題,因?yàn)槟承┍碇杏肋h(yuǎn)不可能有數(shù)據(jù),最大數(shù)據(jù)偏斜率達(dá)到了無(wú)窮大。


那么很明顯,該方案是一個(gè)未達(dá)到預(yù)期效果的錯(cuò)誤方案。數(shù)據(jù)的散落情況大致示意圖如下:


你分庫(kù)分表的姿勢(shì)對(duì)么?——詳談水平分庫(kù)分表


事實(shí)上,只要庫(kù)數(shù)量和表數(shù)量非互質(zhì)關(guān)系,都會(huì)出現(xiàn)某些表中無(wú)數(shù)據(jù)的問(wèn)題。


證明如下:

你分庫(kù)分表的姿勢(shì)對(duì)么?——詳談水平分庫(kù)分表


那么是不是只要庫(kù)數(shù)量和表數(shù)量互質(zhì)就可用用這種分庫(kù)分表方案呢?比如我用11庫(kù)100表的方案,是不是就合理了呢?


答案是否定的,我們除了要考慮數(shù)據(jù)偏斜的問(wèn)題,還需要考慮可持續(xù)性擴(kuò)容的問(wèn)題,一般這種Hash分庫(kù)分表的方案后期的擴(kuò)容方式都是通過(guò)翻倍擴(kuò)容法,那11庫(kù)翻倍后,和100又不再互質(zhì)。


當(dāng)然,如果分庫(kù)數(shù)和分表數(shù)不僅互質(zhì),而且分表數(shù)為奇數(shù)(例如10庫(kù)101表),則理論上可以使用該方案,但是我想大部分人可能都會(huì)覺(jué)得使用奇數(shù)的分表數(shù)比較奇怪吧。


常見(jiàn)錯(cuò)誤案例二:擴(kuò)容難以持續(xù)


如果避開(kāi)了上述案例一的陷阱,那么我們又很容易一頭扎進(jìn)另一個(gè)陷阱,大概思路如下;


我們把10庫(kù)100表看成總共1000個(gè)邏輯表,將求得的Hash值對(duì)1000取余,得到一個(gè)介于[0,999)中的數(shù),然后再將這個(gè)數(shù)二次均分到每個(gè)庫(kù)和每個(gè)表中,大概邏輯代碼如下:

public static ShardCfg shard(String userId) { // ① 算Hash int hash = userId.hashCode(); // ② 總分片數(shù) int sumSlot = DB_CNT * TBL_CNT; // ③ 分片序號(hào) int slot = Math.abs(hash % sumSlot); // ④ 計(jì)算庫(kù)序號(hào)和表序號(hào)的錯(cuò)誤案例 int dbIdx = slot % DB_CNT ; int tblIdx = slot / DB_CNT ; return new ShardCfg(dbIdx, tblIdx); }

該方案確實(shí)很巧妙的解決了數(shù)據(jù)偏斜的問(wèn)題,只要Hash值足夠均勻,那么理論上分配序號(hào)也會(huì)足夠平均,于是每個(gè)庫(kù)和表中的數(shù)據(jù)量也能保持較均衡的狀態(tài)。


你分庫(kù)分表的姿勢(shì)對(duì)么?——詳談水平分庫(kù)分表


但是該方案有個(gè)比較大的問(wèn)題,那就是在計(jì)算表序號(hào)的時(shí)候,依賴了總庫(kù)的數(shù)量,那么后續(xù)翻倍擴(kuò)容法進(jìn)行擴(kuò)容時(shí),會(huì)出現(xiàn)擴(kuò)容前后數(shù)據(jù)不在同一個(gè)表中,從而無(wú)法實(shí)施。


如上圖中,例如擴(kuò)容前Hash為1986的數(shù)據(jù)應(yīng)該存放在6庫(kù)98表,但是翻倍擴(kuò)容成20庫(kù)100表后,它分配到了6庫(kù)99表,表序號(hào)發(fā)生了偏移。這樣的話,我們?cè)诤罄m(xù)在擴(kuò)容的時(shí)候,不僅要基于庫(kù)遷移數(shù)據(jù),還要基于表遷移數(shù)據(jù),非常麻煩且易錯(cuò)。


看完了上面的幾種典型的錯(cuò)誤案例,那么我們有哪些比較正確的方案呢?下面將結(jié)合一些實(shí)際場(chǎng)景案例介紹幾種Hash分庫(kù)分表的方案。


常用姿勢(shì)一:標(biāo)準(zhǔn)的二次分片法


上述錯(cuò)誤案例二中,整體思路完全正確,只是最后計(jì)算庫(kù)序號(hào)和表序號(hào)的時(shí)候,使用了庫(kù)數(shù)量作為影響表序號(hào)的因子,導(dǎo)致擴(kuò)容時(shí)表序號(hào)偏移而無(wú)法進(jìn)行。


事實(shí)上,我們只需要換種寫(xiě)法,就能得出一個(gè)比較大眾化的分庫(kù)分表方案。

public static ShardCfg shard2(String userId) { // ① 算Hash int hash = userId.hashCode(); // ② 總分片數(shù) int sumSlot = DB_CNT * TBL_CNT; // ③ 分片序號(hào) int slot = Math.abs(hash % sumSlot); // ④ 重新修改二次求值方案 int dbIdx = slot / TBL_CNT ; int tblIdx = slot % TBL_CNT ; return new ShardCfg(dbIdx, tblIdx); }

大家可以注意到,和錯(cuò)誤案例二中的區(qū)別就是通過(guò)分配序號(hào)重新計(jì)算庫(kù)序號(hào)和表序號(hào)的邏輯發(fā)生了變化。它的分配情況如下:


你分庫(kù)分表的姿勢(shì)對(duì)么?——詳談水平分庫(kù)分表


那為何使用這種方案就能夠有很好的擴(kuò)展持久性呢?我們進(jìn)行一個(gè)簡(jiǎn)短的證明:

你分庫(kù)分表的姿勢(shì)對(duì)么?——詳談水平分庫(kù)分表


通過(guò)上面結(jié)論我們知道,通過(guò)翻倍擴(kuò)容后,我們的表序號(hào)一定維持不變,庫(kù)序號(hào)可能還是在原來(lái)庫(kù),也可能平移到了新庫(kù)中(原庫(kù)序號(hào)加上原分庫(kù)數(shù)),完全符合我們需要的擴(kuò)容持久性方案。


你分庫(kù)分表的姿勢(shì)對(duì)么?——詳談水平分庫(kù)分表


【方案缺點(diǎn)】


1、翻倍擴(kuò)容法前期操作性高,但是后續(xù)如果分庫(kù)數(shù)已經(jīng)是大幾十的時(shí)候,每次擴(kuò)容都非常耗費(fèi)資源。


2、連續(xù)的分片鍵Hash值大概率會(huì)散落在相同的庫(kù)中,某些業(yè)務(wù)可能容易存在庫(kù)熱點(diǎn)(例如新生成的用戶Hash相鄰且遞增,且新增用戶又是高概率的活躍用戶,那么一段時(shí)間內(nèi)生成的新用戶都會(huì)集中在相鄰的幾個(gè)庫(kù)中)。


常用姿勢(shì)二:關(guān)系表冗余


我們可以將分片鍵對(duì)應(yīng)庫(kù)的關(guān)系通過(guò)關(guān)系表記錄下來(lái),我們把這張關(guān)系表稱為"路由關(guān)系表"。

public static ShardCfg shard(String userId) { int tblIdx = Math.abs(userId.hashCode() % TBL_CNT); // 從緩存獲取 Integer dbIdx = loadFromCache(userId); if (null == dbIdx) { // 從路由表獲取 dbIdx = loadFromRouteTable(userId); if (null != dbIdx) { // 保存到緩存 saveRouteCache(userId, dbIdx); } } if (null == dbIdx) { // 此處可以自由實(shí)現(xiàn)計(jì)算庫(kù)的邏輯 dbIdx = selectRandomDbIdx(); saveToRouteTable(userId, dbIdx); saveRouteCache(userId, dbIdx); } return new ShardCfg(dbIdx, tblIdx); }

該方案還是通過(guò)常規(guī)的Hash算法計(jì)算表序號(hào),而計(jì)算庫(kù)序號(hào)時(shí),則從路由表讀取數(shù)據(jù)。因?yàn)樵诿看螖?shù)據(jù)查詢時(shí),都需要讀取路由表,故我們需要將分片鍵和庫(kù)序號(hào)的對(duì)應(yīng)關(guān)系記錄同時(shí)維護(hù)在緩存中以提升性能。


上述實(shí)例中selectRandomDbIdx方法作用為生成該分片鍵對(duì)應(yīng)的存儲(chǔ)庫(kù)序號(hào),這邊可以非常靈活的動(dòng)態(tài)配置。例如可以為每個(gè)庫(kù)指定一個(gè)權(quán)重,權(quán)重大的被選中的概率更高,權(quán)重配置成0則可以將關(guān)閉某些庫(kù)的分配。當(dāng)發(fā)現(xiàn)數(shù)據(jù)存在偏斜時(shí),也可以調(diào)整權(quán)重使得各個(gè)庫(kù)的使用量調(diào)整趨向接近。


該方案還有個(gè)優(yōu)點(diǎn),就是理論上后續(xù)進(jìn)行擴(kuò)容的時(shí)候,僅需要掛載上新的數(shù)據(jù)庫(kù)節(jié)點(diǎn),將權(quán)重配置成較大值即可,無(wú)需進(jìn)行任何的數(shù)據(jù)遷移即可完成。


如下圖所示:最開(kāi)始我們?yōu)?個(gè)數(shù)據(jù)庫(kù)分配了相同的權(quán)重,理論上落在每個(gè)庫(kù)的數(shù)據(jù)概率均等。但是由于用戶也有高頻低頻之分,可能某些庫(kù)的數(shù)據(jù)增長(zhǎng)會(huì)比較快。當(dāng)掛載新的數(shù)據(jù)庫(kù)節(jié)點(diǎn)后,我們靈活的調(diào)整了每個(gè)庫(kù)的新權(quán)重。


你分庫(kù)分表的姿勢(shì)對(duì)么?——詳談水平分庫(kù)分表


該方案似乎解決了很多問(wèn)題,那么它有沒(méi)有什么不適合的場(chǎng)景呢?當(dāng)然有,該方案在很多場(chǎng)景下其實(shí)并不太適合,以下舉例說(shuō)明。


a、每次讀取數(shù)據(jù)需要訪問(wèn)路由表,雖然使用了緩存,但是還是有一定的性能損耗。


b、路由關(guān)系表的存儲(chǔ)方面,有些場(chǎng)景并不合適。例如上述案例中用戶id的規(guī)模大概是在10億以內(nèi),我們用單庫(kù)百表存儲(chǔ)該關(guān)系表即可。但如果例如要用文件MD5摘要值作為分片鍵,因?yàn)闃颖炯^(guò)大,無(wú)法為每個(gè)md5值都去指定關(guān)系(當(dāng)然我們也可以使用md5前N位來(lái)存儲(chǔ)關(guān)系)。


c、饑餓占位問(wèn)題,如下詳敘:


我們知道,該方案的特點(diǎn)是后續(xù)無(wú)需擴(kuò)容,可以隨時(shí)修改權(quán)重調(diào)整每個(gè)庫(kù)的存儲(chǔ)增長(zhǎng)速度。但是這個(gè)愿景是比較縹緲,并且很難實(shí)施的,我們選取一個(gè)簡(jiǎn)單的業(yè)務(wù)場(chǎng)景考慮以下幾個(gè)問(wèn)題。


【業(yè)務(wù)場(chǎng)景】:以用戶存放文件到云端的云盤業(yè)務(wù)為例,需要對(duì)用戶的文件信息進(jìn)行分庫(kù)分表設(shè)計(jì),有以下假定場(chǎng)景:


  • ①假定有2億理論用戶,假設(shè)當(dāng)前有3000W有效用戶。

  • ②平均每個(gè)用戶文件量級(jí)在2000個(gè)以內(nèi)

  • ③用戶id為隨機(jī)16位字符串

  • ④初期為10庫(kù),每個(gè)庫(kù)100張表。


我們使用路由表記錄每個(gè)用戶所在的庫(kù)序號(hào)信息。那么該方案會(huì)有以下問(wèn)題:


第一:我們總共有2億個(gè)用戶,只有3000W個(gè)產(chǎn)生過(guò)事務(wù)的用戶。若程序不加處理,用戶發(fā)起任何請(qǐng)求則創(chuàng)建路由表數(shù)據(jù),會(huì)導(dǎo)致為大量實(shí)際沒(méi)有事務(wù)數(shù)據(jù)的用戶提前創(chuàng)建路由表。


筆者最初存儲(chǔ)云盤用戶數(shù)據(jù)的時(shí)候便遇到了這個(gè)問(wèn)題,客戶端app會(huì)在首頁(yè)查詢用戶空間使用情況,這樣導(dǎo)致幾乎一開(kāi)始就為每個(gè)使用者分配好了路由。隨著時(shí)間的推移,這部分沒(méi)有數(shù)據(jù)的"靜默"的用戶,隨時(shí)可能開(kāi)始他的云盤使用之旅而“復(fù)蘇”,從而導(dǎo)致它所在的庫(kù)迅速增長(zhǎng)并超過(guò)單個(gè)庫(kù)的空間容量極限,從而被迫拆分?jǐn)U容。


解決這個(gè)問(wèn)題的方案,其實(shí)就是只針對(duì)事務(wù)操作(例如購(gòu)買空間,上傳數(shù)據(jù),創(chuàng)建文件夾等等)才進(jìn)行路由的分配,這樣對(duì)代碼層面便有了一些傾入。


第二、按照前面描述的業(yè)務(wù)場(chǎng)景,一個(gè)用戶最終平均有2000條數(shù)據(jù),假定每行大小為1K,為了保證B 數(shù)的層級(jí)在3層,我們限制每張表的數(shù)據(jù)量在2000W,分表數(shù)為100的話,可以得到理論上每個(gè)庫(kù)的用戶數(shù)不能超過(guò)100W個(gè)用戶。


也就是如果是3000W個(gè)產(chǎn)生過(guò)事務(wù)的用戶,我們需要為其分配30個(gè)庫(kù),這樣會(huì)在業(yè)務(wù)前期,用戶平均數(shù)據(jù)量相對(duì)較少的時(shí)候,存在非常大的數(shù)據(jù)庫(kù)資源的浪費(fèi)。


解決第二個(gè)問(wèn)題,我們一般可以將很多數(shù)據(jù)庫(kù)放在一個(gè)實(shí)例上,后續(xù)隨著增長(zhǎng)情況進(jìn)行拆分。也可以后續(xù)針對(duì)將滿的庫(kù),使用常規(guī)手段進(jìn)行拆分和遷移。


常用姿勢(shì)三:基因法


還是由錯(cuò)誤案例一啟發(fā),我們發(fā)現(xiàn)案例一不合理的主要原因,就是因?yàn)閹?kù)序號(hào)和表序號(hào)的計(jì)算邏輯中,有公約數(shù)這個(gè)因子在影響庫(kù)表的獨(dú)立性。


那么我們是否可以換一種思路呢?我們使用相對(duì)獨(dú)立的Hash值來(lái)計(jì)算庫(kù)序號(hào)和表序號(hào)。

public static ShardCfg shard(String userId) { int dbIdx = Math.abs(userId.substring(0, 4).hashCode() % DB_CNT ); int tblIdx = Math.abs(userId.hashCode() % TBL_CNT); return new ShardCfg(dbIdx, tblIdx);}

如上所示,我們計(jì)算庫(kù)序號(hào)的時(shí)候做了部分改動(dòng),我們使用分片鍵的前四位作為Hash值來(lái)計(jì)算庫(kù)序號(hào)。


這也是一種常用的方案,我們稱為基因法,即使用原分片鍵中的某些基因(例如前四位)作為庫(kù)的計(jì)算因子,而使用另外一些基因作為表的計(jì)算因子。該方案也是網(wǎng)上不少的實(shí)踐方案或者是其變種,看起來(lái)非常巧妙的解決了問(wèn)題,然而在實(shí)際生成過(guò)程中還是需要慎重。


筆者曾在云盤的空間模塊的分庫(kù)分表實(shí)踐中采用了該方案,使用16庫(kù)100表拆分?jǐn)?shù)據(jù),上線初期數(shù)據(jù)正常。然而當(dāng)數(shù)據(jù)量級(jí)增長(zhǎng)起來(lái)后,發(fā)現(xiàn)每個(gè)庫(kù)的用戶數(shù)量嚴(yán)重不均等,故猜測(cè)該方案存在一定的數(shù)據(jù)偏斜。


為了驗(yàn)證觀點(diǎn),進(jìn)行如下測(cè)試,隨機(jī)2億個(gè)用戶id(16位的隨機(jī)字符串),針對(duì)不同的M庫(kù)N表方案,重復(fù)若干次后求平均值得到結(jié)論如下:

8庫(kù)100表min=248305(dbIdx=2, tblIdx=64), max=251419(dbIdx=7, tblIdx=8), rate= 1.25% √16庫(kù)100表min=95560(dbIdx=8, tblIdx=42), max=154476(dbIdx=0, tblIdx=87), rate= 61.65% ×20庫(kù)100表min=98351(dbIdx=14, tblIdx=78), max=101228(dbIdx=6, tblIdx=71), rate= 2.93%

我們發(fā)現(xiàn)該方案中,分庫(kù)數(shù)為16,分表數(shù)為100,數(shù)量最小行數(shù)僅為10W不到,但是最多的已經(jīng)達(dá)到了15W ,最大數(shù)據(jù)偏斜率高達(dá)61%。按這個(gè)趨勢(shì)發(fā)展下去,后期很可能出現(xiàn)一臺(tái)數(shù)據(jù)庫(kù)容量已經(jīng)使用滿,而另一臺(tái)還剩下30% 的容量。


該方案并不是一定不行,而是我們?cè)诓捎玫臅r(shí)候,要綜合分片鍵的樣本規(guī)則,選取的分片鍵前綴位數(shù),庫(kù)數(shù)量,表數(shù)量,四個(gè)變量對(duì)最終的偏斜率都有影響。


例如上述例子中,如果不是16庫(kù)100表,而是8庫(kù)100表,或者20庫(kù)100表,數(shù)據(jù)偏斜率都能降低到了5%以下的可接受范圍。所以該方案的隱藏的"坑"較多,我們不僅要估算上線初期的偏斜率,還需要測(cè)算若干次翻倍擴(kuò)容后的數(shù)據(jù)偏斜率。


例如你用著初期比較完美的8庫(kù)100表的方案,后期擴(kuò)容成16庫(kù)100表的時(shí)候,麻煩就接踵而至。


常用姿勢(shì)四:剔除公因數(shù)法


還是基于錯(cuò)誤案例一啟發(fā),在很多場(chǎng)景下我們還是希望相鄰的Hash能分到不同的庫(kù)中。就像N庫(kù)單表的時(shí)候,我們計(jì)算庫(kù)序號(hào)一般直接用Hash值對(duì)庫(kù)數(shù)量取余。


那么我們是不是可以有辦法去除掉公因數(shù)的影響呢?下面為一個(gè)可以考慮的實(shí)現(xiàn)案例:

public static ShardCfg shard(String userId) { int dbIdx = Math.abs(userId.hashCode() % DB_CNT); // 計(jì)算表序號(hào)時(shí)先剔除掉公約數(shù)的影響 int tblIdx = Math.abs((userId.hashCode() / TBL_CNT) % TBL_CNT); return new ShardCfg(dbIdx, tblIdx);}

經(jīng)過(guò)測(cè)算,該方案的最大數(shù)據(jù)偏斜度也比較小,針對(duì)不少業(yè)務(wù)從N庫(kù)1表升級(jí)到N庫(kù)M表下,需要維護(hù)庫(kù)序號(hào)不變的場(chǎng)景下可以考慮。


常用姿勢(shì)五:一致性Hash法


一致性Hash算法也是一種比較流行的集群數(shù)據(jù)分區(qū)算法,比如RedisCluster即是通過(guò)一致性Hash算法,使用16384個(gè)虛擬槽節(jié)點(diǎn)進(jìn)行每個(gè)分片數(shù)據(jù)的管理。關(guān)于一致性Hash的具體原理這邊不再重復(fù)描述,讀者可以自行翻閱資料。


這邊詳細(xì)介紹如何使用一致性Hash進(jìn)行分庫(kù)分表的設(shè)計(jì)。


我們通常會(huì)將每個(gè)實(shí)際節(jié)點(diǎn)的配置持久化在一個(gè)配置項(xiàng)或者是數(shù)據(jù)庫(kù)中,應(yīng)用啟動(dòng)時(shí)或者是進(jìn)行切換操作的時(shí)候會(huì)去加載配置。配置一般包括一個(gè)[StartKey,Endkey)的左閉右開(kāi)區(qū)間和一個(gè)數(shù)據(jù)庫(kù)節(jié)點(diǎn)信息,例如:


你分庫(kù)分表的姿勢(shì)對(duì)么?——詳談水平分庫(kù)分表


示例代碼:

private TreeMap nodeTreeMap = new TreeMap<>(); @Overridepublic void afterPropertiesSet() { // 啟動(dòng)時(shí)加載分區(qū)配置 List cfgList = fetchCfgFromDb(); for (HashCfg cfg : cfgList) { nodeTreeMap.put(cfg.endKey, cfg.nodeIdx); }} public ShardCfg shard(String userId) { int hash = userId.hashCode(); int dbIdx = nodeTreeMap.tailMap((long) hash, false).firstEntry().getValue(); int tblIdx = Math.abs(hash % 100); return new ShardCfg(dbIdx, tblIdx);}

我們可以看到,這種形式和上文描述的Range分表非常相似,Range分庫(kù)分表方式針對(duì)分片鍵本身劃分范圍,而一致性Hash是針對(duì)分片鍵的Hash值進(jìn)行范圍配置。


正規(guī)的一致性Hash算法會(huì)引入虛擬節(jié)點(diǎn),每個(gè)虛擬節(jié)點(diǎn)會(huì)指向一個(gè)真實(shí)的物理節(jié)點(diǎn)。這樣設(shè)計(jì)方案主要是能夠在加入新節(jié)點(diǎn)后的時(shí)候,可以有方案保證每個(gè)節(jié)點(diǎn)遷移的數(shù)據(jù)量級(jí)和遷移后每個(gè)節(jié)點(diǎn)的壓力保持幾乎均等。


但是用在分庫(kù)分表上,一般大部分都只用實(shí)際節(jié)點(diǎn),引入虛擬節(jié)點(diǎn)的案例不多,主要有以下原因:

a、應(yīng)用程序需要花費(fèi)額外的耗時(shí)和內(nèi)存來(lái)加載虛擬節(jié)點(diǎn)的配置信息。如果虛擬節(jié)點(diǎn)較多,內(nèi)存的占用也會(huì)有些不太樂(lè)觀。


b、由于mysql有非常完善的主從復(fù)制方案,與其通過(guò)從各個(gè)虛擬節(jié)點(diǎn)中篩選需要遷移的范圍數(shù)據(jù)進(jìn)行遷移,不如通過(guò)從庫(kù)升級(jí)方式處理后再刪除冗余數(shù)據(jù)簡(jiǎn)單可控。


c、虛擬節(jié)點(diǎn)主要解決的痛點(diǎn)是節(jié)點(diǎn)數(shù)據(jù)搬遷過(guò)程中各個(gè)節(jié)點(diǎn)的負(fù)載不均衡問(wèn)題,通過(guò)虛擬節(jié)點(diǎn)打散到各個(gè)節(jié)點(diǎn)中均攤壓力進(jìn)行處理。


而作為OLTP數(shù)據(jù)庫(kù),我們很少需要突然將某個(gè)數(shù)據(jù)庫(kù)下線,新增節(jié)點(diǎn)后一般也不會(huì)從0開(kāi)始從其他節(jié)點(diǎn)搬遷數(shù)據(jù),而是前置準(zhǔn)備好大部分?jǐn)?shù)據(jù)的方式,故一般來(lái)說(shuō)沒(méi)有必要引入虛擬節(jié)點(diǎn)來(lái)增加復(fù)雜度。


四、常見(jiàn)擴(kuò)容方案


4.1 翻倍擴(kuò)容法


翻倍擴(kuò)容法的主要思維是每次擴(kuò)容,庫(kù)的數(shù)量均翻倍處理,而翻倍的數(shù)據(jù)源通常是由原數(shù)據(jù)源通過(guò)主從復(fù)制方式得到的從庫(kù)升級(jí)成主庫(kù)提供服務(wù)的方式。故有些文檔將其稱作"從庫(kù)升級(jí)法"。


理論上,經(jīng)過(guò)翻倍擴(kuò)容法后,我們會(huì)多一倍的數(shù)據(jù)庫(kù)用來(lái)存儲(chǔ)數(shù)據(jù)和應(yīng)對(duì)流量,原先數(shù)據(jù)庫(kù)的磁盤使用量也將得到一半空間的釋放。如下圖所示:


你分庫(kù)分表的姿勢(shì)對(duì)么?——詳談水平分庫(kù)分表


具體的流程大致如下:


①、時(shí)間點(diǎn)t1:為每個(gè)節(jié)點(diǎn)都新增從庫(kù),開(kāi)啟主從同步進(jìn)行數(shù)據(jù)同步。


②、時(shí)間點(diǎn)t2:主從同步完成后,對(duì)主庫(kù)進(jìn)行禁寫(xiě)。

此處禁寫(xiě)主要是為了保證數(shù)據(jù)的正確性。若不進(jìn)行禁寫(xiě)操作,在以下兩個(gè)時(shí)間窗口期內(nèi)將出現(xiàn)數(shù)據(jù)不一致的問(wèn)題:


a、斷開(kāi)主從后,若主庫(kù)不禁寫(xiě),主庫(kù)若還有數(shù)據(jù)寫(xiě)入,這部分?jǐn)?shù)據(jù)將無(wú)法同步到從庫(kù)中。

?b、應(yīng)用集群識(shí)別到分庫(kù)數(shù)翻倍的時(shí)間點(diǎn)無(wú)法嚴(yán)格一致,在某個(gè)時(shí)間點(diǎn)可能兩臺(tái)應(yīng)用使用不同的分庫(kù)數(shù),運(yùn)算到不同的庫(kù)序號(hào),導(dǎo)致錯(cuò)誤寫(xiě)入。


③、時(shí)間點(diǎn)t3:同步完全完成后,斷開(kāi)主從關(guān)系,理論上此時(shí)從庫(kù)和主庫(kù)有著完全一樣的數(shù)據(jù)集。


④、時(shí)間點(diǎn)t4:從庫(kù)升級(jí)為集群節(jié)點(diǎn),業(yè)務(wù)應(yīng)用識(shí)別到新的分庫(kù)數(shù)后,將應(yīng)用新的路由算法。

一般情況下,我們將分庫(kù)數(shù)的配置放到配置中心中,當(dāng)上述三個(gè)步驟完成后,我們修改分庫(kù)數(shù)進(jìn)行翻倍,應(yīng)用生效后,應(yīng)用服務(wù)將使用新的配置。這里需要注意的是,業(yè)務(wù)應(yīng)用接收到新的配置的時(shí)間點(diǎn)不一定一致,所以必定存在一個(gè)時(shí)間窗口期,該期間部分機(jī)器使用原分庫(kù)數(shù),部分節(jié)點(diǎn)使用新分庫(kù)數(shù)。這也正是我們的禁寫(xiě)操作一定要在此步完成后才能放開(kāi)的原因。

⑤、時(shí)間點(diǎn)t5:確定所有的應(yīng)用均接受到庫(kù)總數(shù)的配置后,放開(kāi)原主庫(kù)的禁寫(xiě)操作,此時(shí)應(yīng)用完全恢復(fù)服務(wù)。


⑥、啟動(dòng)離線的定時(shí)任務(wù),清除各庫(kù)中的約一半冗余數(shù)據(jù)。

為了節(jié)省磁盤的使用率,我們可以選擇離線定時(shí)任務(wù)清除冗余的數(shù)據(jù)。也可以在業(yè)務(wù)初期表結(jié)構(gòu)設(shè)計(jì)的時(shí)候,將索引鍵的Hash值存為一個(gè)字段。
那么以上述常用姿勢(shì)四為例,我們離線的清除任務(wù)可以簡(jiǎn)單的通過(guò)sql即可實(shí)現(xiàn)(需要防止鎖住全表,可以拆分成若干個(gè)id范圍的子sql執(zhí)行):


delete from db0.tbl0 where hash_val mod 4 <> 0;?

delete from db1.tbl0 where hash_val mod 4 <> 1;

delete from db2.tbl0 where hash_val mod 4 <> 2;

delete from db3.tbl0 where hash_val mod 4 <> 3;


具體的擴(kuò)容步驟可參考下圖:


你分庫(kù)分表的姿勢(shì)對(duì)么?——詳談水平分庫(kù)分表


總結(jié):通過(guò)上述遷移方案可以看出,從時(shí)間點(diǎn)t2到t5時(shí)間窗口呢內(nèi),需要對(duì)數(shù)據(jù)庫(kù)禁寫(xiě),相當(dāng)于是該時(shí)間范圍內(nèi)服務(wù)器是部分有損的,該階段整體耗時(shí)差不多是在分鐘級(jí)范圍內(nèi)。若業(yè)務(wù)可以接受,可以在業(yè)務(wù)低峰期進(jìn)行該操作。


當(dāng)然也會(huì)有不少應(yīng)用無(wú)法容忍分鐘級(jí)寫(xiě)入不可用,例如寫(xiě)操作遠(yuǎn)遠(yuǎn)大于讀操作的應(yīng)用,此時(shí)可以結(jié)合canel開(kāi)源框架進(jìn)行窗口期內(nèi)數(shù)據(jù)雙寫(xiě)操作以保證數(shù)據(jù)的一致性。


該方案主要借助于mysql強(qiáng)大完善的主從同步機(jī)制,能在事前提前準(zhǔn)備好新的節(jié)點(diǎn)中大部分需要的數(shù)據(jù),節(jié)省大量的人為數(shù)據(jù)遷移操作。


但是缺點(diǎn)也很明顯,一是過(guò)程中整個(gè)服務(wù)可能需要以有損為代價(jià),二是每次擴(kuò)容均需要對(duì)庫(kù)數(shù)量進(jìn)行翻倍,會(huì)提前浪費(fèi)不少的數(shù)據(jù)庫(kù)資源。


4.2 一致性Hash擴(kuò)容


我們主要還是看下不帶虛擬槽的一致性Hash擴(kuò)容方法,假如當(dāng)前數(shù)據(jù)庫(kù)節(jié)點(diǎn)DB0負(fù)載或磁盤使用過(guò)大需要擴(kuò)容,我們通過(guò)擴(kuò)容可以達(dá)到例如下圖的效果。


下圖中,擴(kuò)容前配置了三個(gè)Hash分段,發(fā)現(xiàn)[-Inf,-10000)范圍內(nèi)的的數(shù)據(jù)量過(guò)大或者壓力過(guò)高時(shí),需要對(duì)其進(jìn)行擴(kuò)容。


你分庫(kù)分表的姿勢(shì)對(duì)么?——詳談水平分庫(kù)分表


主要步驟如下:


①、時(shí)間點(diǎn)t1:針對(duì)需要擴(kuò)容的數(shù)據(jù)庫(kù)節(jié)點(diǎn)增加從節(jié)點(diǎn),開(kāi)啟主從同步進(jìn)行數(shù)據(jù)同步。


②、時(shí)間點(diǎn)t2:完成主從同步后,對(duì)原主庫(kù)進(jìn)行禁寫(xiě)。

?此處原因和翻倍擴(kuò)容法類似,需要保證新的從庫(kù)和原來(lái)主庫(kù)中數(shù)據(jù)的一致性。

③、時(shí)間點(diǎn)t3:同步完全完成后,斷開(kāi)主從關(guān)系,理論上此時(shí)從庫(kù)和主庫(kù)有著完全一樣的數(shù)據(jù)集。


④、時(shí)間點(diǎn)t4:修改一致性Hash范圍的配置,并使應(yīng)用服務(wù)重新讀取并生效。


⑤、時(shí)間點(diǎn)t5:確定所有的應(yīng)用均接受到新的一致性Hash范圍配置后,放開(kāi)原主庫(kù)的禁寫(xiě)操作,此時(shí)應(yīng)用完全恢復(fù)服務(wù)。


⑥、啟動(dòng)離線的定時(shí)任務(wù),清除冗余數(shù)據(jù)。


可以看到,該方案和翻倍擴(kuò)容法的方案比較類似,但是它更加靈活,可以根據(jù)當(dāng)前集群每個(gè)節(jié)點(diǎn)的壓力情況選擇性擴(kuò)容,而無(wú)需整個(gè)集群同時(shí)翻倍進(jìn)行擴(kuò)容。


五、小結(jié)


本文主要描述了我們進(jìn)行水平分庫(kù)分表設(shè)計(jì)時(shí)的一些常見(jiàn)方案。


我們?cè)谶M(jìn)行分庫(kù)分表設(shè)計(jì)時(shí),可以選擇例如范圍分表,Hash分表,路由表,或者一致性Hash分表等各種方案。進(jìn)行選擇時(shí)需要充分考慮到后續(xù)的擴(kuò)容可持續(xù)性,最大數(shù)據(jù)偏斜率等因素。


文中也列舉了一些常見(jiàn)的錯(cuò)誤示例,例如庫(kù)表計(jì)算邏輯中公約數(shù)的影響,使用前若干位計(jì)算庫(kù)序號(hào)常見(jiàn)的數(shù)據(jù)傾斜因素等等。


我們?cè)趯?shí)際進(jìn)行選擇時(shí),一定要考慮自身的業(yè)務(wù)特點(diǎn),充分驗(yàn)證分片鍵在各個(gè)參數(shù)因子下的數(shù)據(jù)偏斜程度,并提前規(guī)劃考慮好后續(xù)擴(kuò)容的方案。

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
關(guān)閉
關(guān)閉