原文位址:http://jm-blog.aliapp.com/?p=590
目前絕大多數應用采取的兩種分庫分表規則
- mod方式
- dayofweek系列日期方式(所有星期1的資料在一個庫/表,或所有?月份的資料在一個庫表)
這兩種方式有個本質的特點,就是離散性加周期性。
例如以一個表的主鍵對3取餘數的方式分庫或分表:

那麼随着資料量的增大,每個表或庫的資料量都是各自增長。當一個表或庫的資料量增長到了一個極限,要加庫或加表的時候,
介于這種分庫分表算法的離散性,必需要做資料遷移才能完成。例如從3個擴充到5個的時候:
需要将原先以mod3分類的資料,重新以mod5分類,不可避免的帶來資料遷移。每個表的資料都要被重新配置設定到多個新的表
相似的例子比如從dayofweek分的7個庫/表,要擴張為以dayofmonth分的31張庫/表,同樣需要進行資料遷移。
資料遷移帶來的問題是
- 業務至少要兩次釋出
- 要專門寫工具來導資料。由于各業務之間的差别,很難做出統一的工具。目前幾乎都是每個業務寫一套
- 要解決增量、全量、時間點,資料不一緻等問題
如何在資料量擴張到現有庫表極限,加庫加表時避免資料遷移呢?
通常的資料增長往往是随着時間的推移增長的。随着業務的開展,時間的推移,資料量不斷增加。(不随着時間增長的情況,
例如某天突然需要從另一個系統導入大量資料,這種情況完全可以由dba依據現有的分庫分表規則來導入,是以不考慮這種問題。)
考慮到資料增長的特點,如果我們以代表時間增長的字段,按遞增的範圍分庫,則可以避免資料遷移
例如,如果id是随着時間推移而增長的全局sequence,則可以以id的範圍來分庫:(全局sequence可以用tddl現在的方式也可以用ZooKeeper實作)
id在 0–100萬在第一個庫中,100-200萬在第二個中,200-300萬在第3個中 (用M代表百萬資料)
或者以時間字段為例,比如一個字段表示記錄的建立時間,以此字段的時間段分庫gmt_create_time in
range
這樣的方式下,在資料量再增加達到前幾個庫/表的上限時,則繼續水準增加庫表,原先的資料就不需要遷移了
但是這樣的方式會帶來一個熱點問題:目前的資料量達到某個庫表的範圍時,所有的插入操作,都集中在這個庫/表了。
是以在滿足基本業務功能的前提下,分庫分表方案應該盡量避免的兩個問題:
1. 資料遷移
2. 熱點
如何既能避免資料遷移又能避免插入更新的熱點問題呢?
結合離散分庫/分表和連續分庫/分表的優點,如果一定要寫熱點和新資料均勻配置設定在每個庫,同時又保證易于水準擴充,可以考慮這樣的模式:
【水準擴充scale-out方案模式一】
階段一:一個庫DB0之内分4個表,id%4 :
階段二:增加db1庫,t2和t3整表搬遷到db1
階段三:增加DB2和DB3庫,t1整表搬遷到DB2,t3整表搬遷的DB3:
為了規則表達,通過内部名稱映射或其他方式,我們将DB1和DB2的名稱和位置互換得到下圖:
dbRule: “DB” + (id % 4)
tbRule: “t” + (id % 4)
這樣3個階段的擴充方案中,每次次擴容隻需要做一次停機釋出,不需要做資料遷移。停機釋出中隻需要做整表搬遷。
這個相對于每個表中的資料重新配置設定來說,不管是開發做,還是DBA做都會簡單很多。
如果更進一步資料庫的設計和部署上能做到每個表一個硬碟,那麼擴容的過程隻要把原有機器的某一塊硬碟拔下來,
插入到新的機器上,就完成整表搬遷了!可以大大縮短停機時間。
具體在mysql上可以以庫為表。開始一個實體機上啟動4個資料庫執行個體,每次倍增機器,直接将庫搬遷到新的機器上。
這樣從始至終規則都不需要變化,一直都是:
即邏輯上始終保持4庫4表,每個表一個庫。這種做法也是目前店鋪線圖檔空間采用的做法。
上述方案有一個缺點,就是在從一個庫到4個庫的過程中,單表的資料量一直在增長。當單表的資料量超過一定範圍時,可能會帶來性能問題。比如索引的問題,曆史資料清理的問題。
另外當開始預留的表個數用盡,到了4實體庫每庫1個表的階段,再進行擴容的話,不可避免的要從表上下手。那麼我們來考慮表内資料上限不增長的方案:
【水準擴充scale-out方案模式二】
階段一:一個資料庫,兩個表,rule0 = id % 2
分庫規則dbRule: “DB0″
分表規則tbRule: “t” + (id %
2)
階段二:當單庫的資料量接近1千萬,單表的資料量接近500萬時,進行擴容(資料量隻是舉例,具體擴容量要根據資料庫和實際壓力狀況決定):
增加一個資料庫DB1,将DB0.t1整表遷移到新庫DB1。
每個庫各增加1個表,未來10M-20M的資料mod2分别寫入這2個表:t0_1,t1_1:
分庫規則dbRule:
“DB” + (id % 2)
分表規則tbRule:
if(id < 1千萬){
return "t"+ (id % 2); //1千萬之前的資料,仍然放在t0和t1表。t1表從DB0搬遷到DB1庫
}else if(id < 2千萬){
return "t"+ (id % 2)
+"_1"; //1千萬之後的資料,各放到兩個庫的兩個表中: t0_1,t1_1
}else{
throw new
IllegalArgumentException("id outof range[20000000]:" + id);
}
這樣10M以後的新生資料會均勻分布在DB0和DB1; 插入更新和查詢熱點仍然能夠在每個庫中均勻分布。
每個庫中同時有老資料和不斷增長的新資料。每表的資料仍然控制在500萬以下。
階段三:當兩個庫的容量接近上限繼續水準擴充時,進行如下操作:
新增加兩個庫:DB2和DB3. 以id % 4分庫。餘數0、1、2、3分别對應DB的下标. t0和t1不變,
将DB0.t0_1整表遷移到DB2; 将DB1.t1_1整表遷移到DB3
20M-40M的資料mod4分為4個表:t0_2,t1_2,t2_2,t3_2,分别放到4個庫中:
新的分庫分表規則如下:
if(id < 2千萬){
//2千萬之前的資料,4個表分别放到4個庫
if(id < 1千萬){
return "db"+ (id % 2);
//原t0表仍在db0, t1表仍在db1
}else{
return "db"+ ((id % 2) +2);
//原t0_1表從db0搬遷到db2; t1_1表從db1搬遷到db3
}
}else if(id < 4千萬){
return "db"+ (id % 4); //超過2千萬的資料,平均分到4個庫
}else{
throw new
IllegalArgumentException("id out of range. id:"+id);
}
if(id < 2千萬){
//2千萬之前的資料,表規則和原先完全一樣,參見階段二
return "t"+ (id % 2); //1千萬之前的資料,仍然放在t0和t1表
return "t"+ (id % 2)
+"_1"; //1千萬之後的資料,仍然放在t0_1和t1_1表
return "t"+ (id %
4)+"_2"; //超過2千萬的資料分為4個表t0_2,t1_2,t2_2,t3_2
随着時間的推移,當第一階段的t0/t1,第二階段的t0_1/t1_1逐漸成為曆史資料,不再使用時,可以直接truncate掉整個表。省去了曆史資料遷移的麻煩。
上述3個階段的分庫分表規則在TDDL2.x中已經全部支援,具體請咨詢TDDL團隊。
【水準擴充scale-out方案模式三】
非倍數擴充:如果從上文的階段二到階段三不希望一下增加兩個庫呢?嘗試如下方案:
遷移前:
新增庫為DB2,t0、t1都放在DB0,
t0_1整表遷移到DB1
t1_1整表遷移到DB2
遷移後:
這時DB0退化為舊資料的讀庫和更新庫。新增資料的熱點均勻分布在DB1和DB2
4無法整除3,是以如果從4表2庫擴充到3個庫,不做行級别的遷移而又保證熱點均勻分布看似無法完成。
當然如果不限制每庫隻有兩個表,也可以如下實作:
小于10M的t0和t1都放到DB0,以mod2分為兩個表,原資料不變
10M-20M的,以mod2分為兩個表t0_1、t1_1,原資料不變,分别搬遷到DB1,和DB2
20M以上的以mod3平均配置設定到3個DB庫的t_0、t_2、t_3表中
這樣DB1包含最老的兩個表,和最新的1/3資料。DB1和DB2都分表包含次新的兩個舊表t0_1、t1_1和最新的1/3資料。
新舊資料讀寫都可達到均勻分布。
總而言之:
兩種規則映射(函數):
- 離散映射:如mod或dayofweek, 這種類型的映射能夠很好的解決熱點問題,但帶來了資料遷移和曆史資料問題。
- 連續映射;如按id或gmt_create_time的連續範圍做映射。這種類型的映射可以避免資料遷移,但又帶來熱點問題。
離散映射和連續映射這兩種相輔相成的映射規則,正好解決熱點和遷移這一對互相沖突的問題。
我們之前隻運用了離散映射,引入連續映射規則後,兩者結合,精心設計,
應該可以設計出滿足避免熱點和減少遷移之間任意權衡取舍的規則。
基于以上考量,分庫分表規則的設計和配置,長遠說來必須滿足以下要求
- 可以動态推送修改
- 規則可以分層級疊加,舊規則可以在新規則下繼續使用,新規則是舊規則在更寬尺度上的拓展,以此支援新舊規則的相容,避免資料遷移
- 用mod方式時,最好選2的指數級倍分庫分表,這樣友善以後切割。