1. 背景
在關系數據庫中猜揪,當單個庫的負載、連接數拴念、并發(fā)數等達到數據庫的最大上限時政鼠,就得考慮做數據庫和表的拆分。如一個簡單的電商數據庫官帘,在業(yè)務初期,為了快速驗證業(yè)務模式涌哲,把用戶阀圾、商品、訂單都放到一個數據庫中账月,隨著業(yè)務的發(fā)展及用戶量的增長,單數據庫逐漸不能支撐業(yè)務(MySQL中單記錄容量超過1K時抓歼,單表數據量建議不超過一千萬條)谣妻,這時就得考慮把數據庫和表做出拆分。
2. 垂直拆分
所謂垂直拆分减江,就是將數據庫及表由一個拆分為多個,即垂直拆分為用戶數據庫巡莹、商品數據庫和訂單數據庫(垂直拆庫),訂單表可以垂直拆分為訂單基本信息表钉鸯,訂單收貨地址表贸营、訂單商品表(垂直拆表)等钞脂,每一個表里保存了一個訂單的一部分數據
3. 水平拆分
所謂垂直拆分刘莹,就是說在垂直拆庫/表后点弯,單庫/表容量還是太大狼钮,那就將一個庫、一個表水平擴展為多個庫猛蔽,多個表,每一個拆分后的表中保存的依然是一個訂單的完整信息毁枯。如電商數據庫,我們按水平拆分數據庫和表后赂韵,每一個拆分后的數據庫表與現有未拆分前的都保持一致。
4. 拆分鍵
隨著業(yè)務的發(fā)展及用戶量的增長质涛,分庫分表是必然的選擇稠歉,但是分庫分表后,容易遇到的問題汇陆,就是拆分鍵的設計怒炸。
一般情況下,拆分鍵的選取遵循以什么維度進行查詢就選取該維度為拆分鍵毡代。如:訂單表就以訂單號作為拆分鍵,商品表就以商品編號作為拆分鍵教寂。拆分鍵選取后灯蝴,對于一些非拆分鍵的單條件查詢,我們需要怎么支持呢孝宗?
4.1 等值法
對于非拆分鍵的單條件查詢,對這一個單條件的賦值耕肩,可以將其值與拆分鍵保持一致因妇。比如在電商場景中,用戶下訂單后猿诸,需要通過物流給用戶把商品送到用戶手上婚被。對于用戶來說僅能看到訂單信息,訂單上展示的物流信息用戶也是通過訂單號查詢而來梳虽;但對于物流系統(tǒng)來說址芯,其系統(tǒng)里的業(yè)務主鍵(拆分鍵)是運單號,此時窜觉,運單號如果和訂單號相同谷炸,即可完美解決這一問題。訂單表和運單表的基本數據模型如下:
訂單表
運單表
在訂單表中禀挫,拆分鍵order_id與運單表中的拆分鍵waybill_code值相同旬陡,當按訂單號查詢運單表里的運單信息時,可以直接查詢拆分鍵waybill_code獲取訂單對應的運單信息语婴。
小公司可以直接這樣設計描孟,因為訂單和運單都是同一個部門處理,但是稍微大點的公司就不太可能用這種方案砰左,因為需要跨部門協(xié)作匿醒,不同部門不同業(yè)務,很難使用同一個單號來當訂單號和運單號缠导,通常是會有個流水號作為全鏈路唯一廉羔。
4.2 索引法
對于常用的非拆分鍵,我們可以將其與拆分鍵之間建立一個索引關系僻造,當按該條件進行查詢時蜜另,先查詢對應的拆分鍵适室,再通過拆分鍵查詢對應的數據信息。
當查詢用戶(user001)的下單記錄時举瑰,通過用戶編碼先查詢索引表捣辆,查詢出user001的所有下單的訂單號(10001),再通過訂單號查詢訂單表獲取用戶的訂單信息此迅;同理汽畴,根據運單號(Y00232)查詢訂單信息時,在索引表里先查詢到對應的訂單號耸序,再根據訂單號查詢對應的訂單信息忍些。
缺點:
- 需要先從索引表反查一次;
- 這張索引表會很大坎怪,有時候索引表也需要分表罢坝,
4.3 基因法
基因法是對上面等值法的優(yōu)化,等值法要求不同表的拆分鍵值保持一致搅窿,但是實際上不同業(yè)務表的拆分鍵值是有其業(yè)務意義的嘁酿。
但是我們可以讓不同拆分鍵存在相同規(guī)則的部分,且這部分用來做拆分鍵男应,從而定位庫表闹司。
4.1 理論基礎
如果我們對一個10進制的數字按10取模,取模的結果與這串數的前面所有位都沒有任何關系沐飘,最后1位決定取模結果:
同理游桩,按100(102)取模,最后2位決定取模結果耐朴,按1000(103)取模借卧,最后3位決定取模結果:
同理:一個二進制的值,按2^n取模筛峭,也是最后n位決定取模結果:
那么我們在生成訂單號的時候谓娃,只要把order_no二進制的最后(n+1)位的二進制數設置為user_id的最后(n)位,那么我們對user_id/order_no取余都能得到相同的結果了蜒滩。(原理:比n位高的值滨达,都是b數的倍數,取余時直接歸零俯艰,所以取余就是取二進制最后n位)
4.2 應用
假如我們現在現有16個庫捡遍,128個表,定位的流程是hash(user_id)% 16 定位庫的位置竹握, hash(user_id)% 128 定位表的位置画株;
所以在我們生成訂單號時,先對user_id提取一個基因, 也就是二進制最后 7位谓传,然后這個最后7位二進制也作為order_no的二進制最后7位蜈项,這樣就能保證order_no的路由結果與user_id完全一致;
通過基因法续挟,我們可以通過非拆分鍵也能快速定位到具體的表紧卒。
4.3 代碼實現
備注:這個demo有點問題
public static void main(String[] args) {
SnowFlakeIdGenerator snowFlakeIdGenerator = new SnowFlakeIdGenerator();
for (int i = 1; i < 1000; i++) {
//生成userId和提取基因
long userId = snowFlakeIdGenerator.generateId();
String userIdGene = fetchGene(userId, 128);
//利用基因生成orderId
long rawOrderId = snowFlakeIdGenerator.generateId();
Long orderId = generateWithGene(rawOrderId, userIdGene);
System.out.println("userId: " + userId + ",余數:" + userId % 128);
System.out.println("orderId: " + rawOrderId + ",余數:" + orderId % 128);
}
}
/**
* 抽取基因
*
* @param id id
* @param index 16/128 需要取余的數
* @return {@link String}
*/
public static String fetchGene(Long id, Integer index) {
return String.format("%07d", Integer.valueOf(Long.toBinaryString(id % index)));
}
/**
* 根據基因,生成id
*
* @param rawId 原始id
* @param binarySuffix 二進制后綴
* @return {@link Long}
*/
public static Long generateWithGene(Long rawId, String binarySuffix) {
String s = Long.toBinaryString(rawId);
String substring = s.substring(0, s.length() - binarySuffix.length() + 1);
String newBinaryString = substring + binarySuffix;
return Long.parseLong(newBinaryString, 2);
}
分庫分表后诗祸,我們通常會采用雪花算法來生成分片鍵跑芳,比如訂單號,商品編號直颅,
我們在雪花算法的圖下加入了訂單id生成的示意圖博个, 比如我們有16個庫,128張表功偿,則需要截取二進制訂單id的最后LOG(128,2)=7位盆佣,作為分庫/分表基因。
然后對訂單id用hash生成60位械荷,加上從用戶id那邊獲取的4位基因共耍,形成最終的訂單id。其他業(yè)務id也使用相同的辦法處理养葵。
分庫/分表策略時,直接設定使用該id進行水平切分瘩缆。由于所有業(yè)務都有相同的最后4位关拒,這樣sharding時都會進入相同的庫/表