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

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

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

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


