分庫分表實(shí)戰(zhàn):可能是用戶表最佳分庫分表方案
再次拋出筆者的觀點(diǎn),在能滿足業(yè)務(wù)場(chǎng)景的情況下,單表>分區(qū)>單庫分表>分庫分表,推薦優(yōu)先級(jí)從左到右逐漸降低。
本篇文章主要講用戶表(或者類似這種業(yè)務(wù)屬性的表)的分表方案,至于訂單表,流水表等,本文的方案可能不是很合適,可以參考筆者另一篇文章《分庫分表技術(shù)演進(jìn)&最佳實(shí)踐-修訂篇》。
我們首先來看一下分表時(shí)主要需要做的事情:
選定分片鍵:既然是用戶表那分片鍵非用戶ID莫屬;
修改代碼:以sharding-jdbc這種client模式的中間件為例,主要是引入依賴,然后新增一些配置。業(yè)務(wù)代碼并不怎么需要改動(dòng)。
存量數(shù)據(jù)遷移;
業(yè)務(wù)發(fā)展超過容量評(píng)估后需要開發(fā)和運(yùn)維介入擴(kuò)容;
做過分庫分表的都知道,第3步最麻煩,而且非常不好驗(yàn)證遷前后數(shù)據(jù)一致性(目前業(yè)界主流的遷移方案是存量數(shù)據(jù)遷移+利用binlog進(jìn)行增量數(shù)據(jù)同步,待兩邊的數(shù)據(jù)持平后,將業(yè)務(wù)代碼中的開關(guān)切到分表模式)。
第4步同樣麻煩,業(yè)務(wù)增長完全超過當(dāng)初分表設(shè)計(jì)的容量評(píng)估是很常見的事情,這也成為業(yè)務(wù)高速發(fā)展的一個(gè)隱患。而且互聯(lián)網(wǎng)類型的業(yè)務(wù)都希望能做到7x24小時(shí)不停服務(wù),這樣就給擴(kuò)容帶來了更大的挑戰(zhàn)。筆者看過比較好的方案就是58沈劍提出的成倍擴(kuò)容方案。如下圖所示,假設(shè)現(xiàn)在已經(jīng)有2張表:tb_user_1,tb_user_2。且有兩個(gè)庫是主備關(guān)系,并且分表算法是hash(user_id)%2:

現(xiàn)在要擴(kuò)容到4張表,做法是將兩個(gè)庫的主從關(guān)系切斷。然后slave晉升為master,這樣就有兩個(gè)主庫:master-1,master-2。新的分表算法是:
庫選擇算法為:hash(user_id)%4的結(jié)果為1或者2,就選master-1庫,hash(user_id)%4的結(jié)果為3或者0,就選master-2庫;
表的選擇算法為:hash(user_id)%2的結(jié)果為1則選tb_user_1表,hash(user_id)%2的結(jié)果為0則選tb_user_2表。
如此以來,兩個(gè)庫中總計(jì)4張表,都冗余了1倍的數(shù)據(jù):master-1中tb_user_1冗余了3、7、11…,master-1中tb_user_2冗余了4、8、12…,master-2中tb_user_1冗余了1、5、9…,master-2中tb_user_2冗余了2、6、10…。將這些冗余數(shù)據(jù)刪掉后,庫、表、數(shù)據(jù)示意圖如下所示:

即使這樣方案,還是避免不了分表時(shí)的存量數(shù)據(jù)遷移,以及分表后業(yè)務(wù)發(fā)展到一定時(shí)期后的繁瑣擴(kuò)容。那么有沒有一種很好的方案,能夠一勞永逸,分表時(shí)不需要存量數(shù)據(jù)遷移,用戶量無論如何增長,擴(kuò)容時(shí)都不需要遷移存量數(shù)據(jù),只需要新增一個(gè)數(shù)據(jù)庫示例,修改一下配置即可。軟件開發(fā)行業(yè),一個(gè)方案能撐過3~5年就是一個(gè)很優(yōu)秀的方案,我們現(xiàn)在YY的是整個(gè)生命周期內(nèi)都不用改動(dòng)的完美的方案。沒錯(cuò),我們?cè)趯ふ毅y彈。
這個(gè)方案筆者在兩個(gè)地方都接觸到了:
某V廠面試時(shí),部門老大提出的方案;
和美團(tuán)大牛普架討論了解到的CAT存儲(chǔ)方案;
說明:CAT是美團(tuán)點(diǎn)評(píng)開源的APM,目前在Github上的star已經(jīng)破萬(Github地址:https://github.com/dianping/cat),比skywalking和pinpoint還快,如果你正在選型APM,而且能接受代碼侵入,那么CAT是一個(gè)不錯(cuò)的選擇。
CAT存儲(chǔ)方案是按照寫入時(shí)間順序存儲(chǔ),假設(shè)每小時(shí)寫入量是千萬級(jí)別,那么分表就按照小時(shí)維度。也就是說,2019年7月18號(hào)10點(diǎn)數(shù)據(jù)寫入到表tb_catdata_2019071810中,2019年7月18號(hào)12點(diǎn)數(shù)據(jù)寫入到表tb_catdata_2019071812中,2019年7月20號(hào)14點(diǎn)數(shù)據(jù)寫入到表tb_catdata_2019072014中。這樣做的優(yōu)點(diǎn)如下:
歷史數(shù)據(jù)不用遷移;
擴(kuò)容非常簡(jiǎn)單;
缺點(diǎn)如下:
讀寫熱點(diǎn)集中,所有寫操作全部打在最新的表上。
有沒有發(fā)現(xiàn),這個(gè)方案的優(yōu)點(diǎn)就是我們需要的。BINGO,要的就是這樣的方案。那么對(duì)應(yīng)到用戶表上來具體的分表方案非常類似:按照range切分。需要說明的是,這個(gè)方案的前提是用戶ID一定要趨勢(shì)遞增,最好嚴(yán)格遞增。筆者給出3種用戶ID遞增的方案:
自增ID
假設(shè)存量數(shù)據(jù)用戶表的id最大值是960W,那么分表算法是這樣的,表序號(hào)只需要根據(jù)user_id/10000000就能得到:
用戶ID在范圍[1, 10000000)中分到tb_user_0中(需要將tb_user重命名為tb_user_0);
用戶ID在范圍[10000000, 20000000)中分到tb_user_1中;
用戶ID在范圍[20000000, 30000000)中分到tb_user_2中;
用戶ID在范圍[30000000, 40000000)中分到tb_user_3中;
以此類推。
如果你的tb_user本來就有自增主鍵,那這種方案就比較好。但是需要注意幾點(diǎn),由于用戶ID是自增的,所以這個(gè)ID不能通過HTTP暴露出去,否則可以通過新注冊(cè)一個(gè)用戶后,就能得到你的真實(shí)用戶數(shù),這是比較危險(xiǎn)的。其次,存量數(shù)據(jù)在單表中可以通過自增ID生成,但是當(dāng)切換分表后,用戶ID如果還是用自增生成,需要注意在創(chuàng)建新表時(shí)設(shè)置AUTO_INCREMENT,例如創(chuàng)建表tb_user_2時(shí),設(shè)置AUTO_INCREMENT=10000000,DDL如下:
CREATE?TABLE?if?not?exists?`tb_user_2`?(
??`id`?int(11)?unsigned?NOT?NULL?AUTO_INCREMENT?PRIMARY?KEY,
??`username`?varchar(16)?NOT?NULL?COMMENT?'用戶名',
??`remark`?varchar(16)?NOT?NULL?COMMENT?'備注'
)?ENGINE=InnoDB?AUTO_INCREMENT=10000000;
-?這樣的話,當(dāng)新增用戶時(shí),用戶ID就會(huì)從10000000開始,而不會(huì)與之前的用戶ID沖突
insert?into?tb_user_2?values(null,?'afei',?'afei');
Redis incr
第二種方案就是利用Redis的incr命令。將之前最大的ID保存到Redis中,接下來新增用戶的ID值都通過incr命令得到。然后insert到表tb_user中。這種方案需要注意Redis主從切換后,晉升為主的Redis節(jié)點(diǎn)中的ID可能由于同步時(shí)間差不是最新ID的問題。這樣的話,可能會(huì)導(dǎo)致插入記錄到tb_user失敗。需要對(duì)這種異常特殊處理一下即可。
利用雪花算法生成
采用類雪花算法生成用戶ID,這種方式不太好精確掌握切分表的時(shí)機(jī)。因?yàn)闆]有高效獲取tb_user表數(shù)據(jù)量的辦法,也就不知道什么時(shí)候表數(shù)據(jù)量達(dá)到1000w級(jí)別,也就不知道什么時(shí)候需要往新表中插入數(shù)據(jù)(select count(*) from tb_user無論怎么優(yōu)化性能都不會(huì)很高,除非是MyISAM引擎)。而且如果利用雪花算法生成用戶ID,那么還需要一張表保存用戶ID和分表關(guān)系:

筆者推薦第一種方案,即利用表自增ID生成用戶ID:方案越簡(jiǎn)單,可靠性越高。其他兩種方案,或者其他方案或多或少需要引入一些中間件或者介質(zhì),從而增加方案的復(fù)雜度。新方案效果圖如下:

回顧總結(jié)
我們回頭看一下這種用戶表方案,滿足了存量數(shù)據(jù)不需要做任何遷移(除非是存量數(shù)據(jù)遠(yuǎn)遠(yuǎn)超過單表承受能力)。而且,無論用戶規(guī)模增長到多大量級(jí),1億,10億,50億,后面都不需要做數(shù)據(jù)遷移。而且也不再需要開發(fā)和運(yùn)維介入。因?yàn)檎麄€(gè)方案,會(huì)自己往新表中插入數(shù)據(jù)。我們唯一需要做的就是,根據(jù)硬件性能,約定一個(gè)庫允許保存的用戶表數(shù)量即可。假如一個(gè)庫保存64張表,那么當(dāng)擴(kuò)容到第65張表時(shí),程序會(huì)自動(dòng)往第二個(gè)庫的第一張表中寫入。
END


