本文主要描述分庫分表的算法方案、按什么規則划分。循序漸進比較目前出現的幾種規則方式,最后第五種增量遷移方案是我設想和推薦的方式。后續章再講述技術選型和分庫分表后帶來的問題。
背景
隨着業務量遞增,數據量遞增,一個表將會存下大量數據,在一個表有一千萬行數據時,通過sql優化、提升機器性能還能承受。為了未來長遠角度應在一定程度時進行分庫分表,如出現數據庫性能瓶頸、增加字段時需要耗時比較長的時間的情況下。解決獨立節點承受所有數據的壓力,分布多個節點,提供容錯性,不必一個掛整個系統不能訪問。
目的
本文講述的分庫分表的方案,是基於水平分割的情況下,選擇不同的規則,比較規則的優缺點。
一般網上就前三種,正常一點的會說第四種,但不是很完美,前面幾種遷移數據都會很大影響,推薦我認為比較好的方案五。
- 方案一:對Key取模,除數逐步遞增
- 方案二:按時間划分
- 方案三:按數值范圍
- 方案四:一致性Hash理念——平均分布方案(大眾點評用這種,200G並且一步到位)
- 方案五:一致性Hash理念——按迭代增加節點(為了方便增量遷移)
- 方案六:一致性Hash理念——按范圍分庫(迭代遷移)
點贊再看,關注公眾號:【地藏思維】給大家分享互聯網場景設計與架構設計方案
掘金:地藏Kelvin https://juejin.im/user/5d67da8d6fb9a06aff5e85f7
方案選擇
方案一:對Key取模,除數逐步遞增
公式:key mod x (x為自然數)
Key可以為主鍵,也可以為訂單號,也可以為用戶id,這個需要根據場景決定,哪個作為查詢條件概率多用哪個。
優點:
- 按需增加庫、表,逐步增加
- 分布均勻,每一片差異不多
缺點:
- 很多時候會先從2開始分兩個庫逐級遞增,然后分3個、4個、5個。如在mod 3 變 mod 5的情況下,取模后的大部分的數據的取模結果會變化,如key=3時,mod 3=0,突然改變為mod 5=3,則將會從第0表遷移到第3表,將會造成很多數據多重復移動位置。
- 會重復遷移數據,當分2個時,有數據A在第0號表,分3個時數據A去了第1號表、到分4個時數據A會回到第0號表
方案二:按時間划分
可以按日、按月、按季度。
tb_20190101
tb_20190102
tb_20190103
……
這個算法要求在訂單號、userId上添加年月日或者時間戳,或者查詢接口帶上年月日,才能定位在哪個分片。
優點:
- 數據按時間連續
- 看數據增長比較直觀
缺點:
- 因為考慮到歷史數據一開始沒分庫分表后續進行分庫分表時,歷史數據的訂單號不一定有時間戳,歷史數據可能為自增或者自定義算法得出的分布式主鍵,導致查詢時必須要上游系統傳訂單號、創建時間兩個字段。
- 若上游系統沒有傳時間,或者上游系統的創建時間與當前系統對應訂單的創建時間不在同一天的情況下,則當前數據庫表的數據記錄需要有時間字段。因為上游系統只傳訂單號,這個時候需要獲取創建時間,當前系統就必須要有一個主表維護訂單號和創建時間的關系,並且每次查詢時都需要先查當前系統主表,再查具體表,這樣就會消耗性能。
- 分布不一定均勻:每月增長數據不一樣,可能會有些月份多有些月份少
推薦使用場景:日志記錄
方案三:按數值范圍
表0 [0,10000000)
表1 [10000000,20000000)
表2 [20000000,30000000)
表3 [30000000,40000000)
……
優點:
- 分布均勻
缺點:
- 因為未知最大值,所以無法用時間戳作為key,這個方法不能用表的自增主鍵,因為每個表都自增數量不是統一維護。所以需要有一個發號器或發號系統做統一維護key自增的地方。
說后續推薦的方案中先簡單說說一致性hash
先說一下一致性hash,有些文章說一致性Hash是一種算法,我認為它並不是具體的計算公式,而是一個設定的思路。
1.先假定一個環形Hash空間,環上有固定最大值和最小值,頭尾相連,形成一個閉環,如int,long的最大值和最小值。
很多文章會假定232個位置,最大值為232-1最小值為0,即0~(2^32)-1的數字空間,他們只是按照常用的hash算法舉例,真實分庫分表的情況下不是用這個數字,所以我才會認為一致性hash算法其實是一個理念,並不是真正的計算公式。
如下圖
2. 設計一個公式函數 value = hash(key),這個公式將會有最大值和最小值,如 key mod 64 = value; 這個公式最大為64,最小為0。然后把數據都落在環上。
3. 設定節點node。設定節點的方式如對ip進行hash,或者自定義固定值(后續方案是使用固定值)。然后node逆時針走,直到前一個節點為止,途經value=hash(key)的所有數據的都歸這個節點管。
如 hash(node1)=10,則hash(key)=0~10的數據都歸node1管。
概括
這里不詳細說明這個理論,它主要表達的意思是固定好最大值,就不再修改最大值到最小值范圍,后續只修改節點node的位置和增加node來達到減少每個node要管的數據,以達到減少壓力。
備注:
* 不推薦對ip進行hash,因為可能會導致hash(ip)得出的結果很大,例如得出60,若這個節點的前面沒有節點,則60號位置的這個節點需要管大部分的數據了。
* 最好生成key的方式用雪花算法snowFlake來做,至少要是不重復的數字,也不要用自增的形式。
* 推薦閱讀銅板街的方案 訂單號末尾添加user%64
方案四:一致性Hash理念——平均分布方案
利用一致性hash理論,分庫選擇hash(key)的公式基准為 value= key mod 64,分表公式value= key / 64 mod 64,key為訂單號或者userId等這類經常查詢的主要字段。(后續會對這個公式有變化)
我們假定上述公式,則可以分64個庫,每個庫64個表,假設一個表1千萬行記錄。則最大64 * 64 * 1000萬數據,我相信不會有這一天的到來,所以我們以這個作為最大值比較合理,甚至選擇32 * 32都可以。
因為前期用不上這么多個表,一開始建立這么多表每個表都insert數據,會造成浪費機器,所以在我們已知最大值的情況下,我們從小的數字開始使用,所以我們將對上述計算得出的value進行分組。
分組公式:64 = 每組多少個count * group需要分組的個數
數據所在環的位置(也就是在哪個庫中):value = key mode 64 / count * count
以下舉例為16組,也就是16個庫,group=16
這個時候庫的公式為value = key mode 64 / 4 * 4,除以4后,會截取小數位得出一個整數,然后 * 4倍,就是數據所在位置。
// 按4個為一組,分兩個表
count = 4:
Integer dbValue = userId % 64 / count * count ;
hash(key)在0~3之間在第0號庫
hash(key)在4~7之間在第4號庫
hash(key)在8~11之間在第8號庫
……
備注:其實一開始可以64個為一組就是一個庫,后續變化32個為一組就是兩個庫,從一個庫到兩個庫,再到4個庫,逐步遞進。
從分1庫開始擴容的迭代:
下圖中舉例分16組后,變為分到32組,需要每個庫都拿出一半的數據遷移到新數據,擴容直到分64個組。
可以看到當需要進行擴容一倍時需要遷移一半的數據量,以2^n遞增,所以進行影響范圍會比較大。
優點:
- 如果直接拆分32組,那么就比較一勞永逸
- 如果數據量比較大,未做過分表可以用一勞永逸方式。
- 分布均勻
- 遷移數據時不需要像方案一那樣大部分的數據都需要進行遷移並有重復遷移,只需要遷移一半
缺點:
- 可以擴展,但是影響范圍大。
- 遷移的數據量比較大,雖然不像方案一那樣大部分數據遷移,當前方案每個表或庫都需要一半數據的遷移。
- 若要一勞永逸,則需要整體停機來遷移數據
方案五:一致性Hash理念——按迭代增加節點
(我認為比較好的方案)
一致性hash方案結合比較范圍方案,也就是方案三和方案四的結合。
解析方案四問題所在
方案四是設定最大范圍64,按2^n指數形式從1增加庫或者表數量,這樣帶來的是每次拆分進行遷移時會影響當總體數據量的1/2的數據,影響范圍比較大,所以要么就直接拆分到32組、64組一勞永逸,要么每次1/2遷移。
方案四對應遷移方案:
- 第一種是停機遷移數據,成功后,再重新啟動服務器。影響范圍為所有用戶,時間長。
- 第二種是把數據源切到從庫,讓用戶只讀,主庫遷移數據,成功后再切到主庫,雖然用戶能適用,影響業務增量
- 第三種是設定數據源根據規則讓一半的用戶能只讀,另一半的用戶能讀能寫,因為方案四遷移都是影響一般的數據的,所以最多能做到這個方式。
方案五詳解
現在我想方法時,保持一致性hash理念,1個1個節點來增加,而不是方案四的每次增加2^n-n個節點。但是代碼上就需要進行對新節點內的數據hash值判斷。
我們基於已經發生過1次迭代分了兩個庫的情況來做后續迭代演示,首先看看已經拆分兩個庫的情況:
數據落在第64號庫名為db64和第32號庫名為db32
迭代二:
區別與方案四直接增加兩個節點,我們只增加一個節點,這樣遷移數據時由原本影響1/2的用戶,將會只影響1/4的用戶。
在代碼中,我們先把分組從32個一組改為16個一組,再給代碼特殊處理
0~16的去到新的節點
16~32走回原來的32號節點
32~63走回原來64號節點
所以下面就要對節點特殊if else
// 按32改為16個為一組,分兩變為4個庫
count = 16;
Integer dbValue = userId % 64 / count * count ;
if(dbValue<16){
// 上一個迭代這些數據落在db32中,現在走新增節點名為db16號的那個庫
dbValue = 16;
return dbValue;
} else {
// 按原來規則走
return dbValue;
}
迭代三:
這樣就可以分迭代完成方案四種的一輪的遷移
遷移前可以先上線,增加一段開關代碼,請求接口特殊處理hash值小於16的訂單號或者用戶號,這樣就只會影響1/4的人
// 在請求接口中增加邏輯
public void doSomeService(Integer userId){
if(遷移是否完成的開關){
// 如果未完成
Integer dbValue = userId % 64 / count * count ;
if(dbValue<16){
//這部分用戶暫時不能走下面的邏輯
return ;
}
}
return dbValue;
}
}
// 在分片時按32個為一組,分兩個庫
count = 16;
Integer dbValue = userId % 64 / count * count ;
if(dbValue<16){
// 上一個迭代這些數據落在db32中,有一半需要走新增節點名為db16號的那個庫
if(遷移是否完成的開關){
// 如果已經完成,就去db16的庫
dbValue = 16;
}
return dbValue;
} else {
// 按原來規則走
return dbValue;
}
如此類推,下一輪總共8個節點時,每次遷移只需要遷移1/8。
其實也可以在第一個迭代時,不選擇dbValue小於16號的來做。直接8個分一組,只選擇dbValue<8的來做,這樣第一個迭代的影響范圍也會比較案例中小。上述案例用16只是比較好演示
優點:
- 易於擴展
- 數據逐漸增大過程中,慢慢增加節點
- 影響用戶數量少
- 按迭代進行,減少風險
- 遷移時間短,如敏捷迭代思想
缺點:
- 一段時間下不均勻
方案六:一致性Hash理念——按范圍分庫(迭代遷移)
如同上述方案五是方案四+方案一,可以達到逐步遷移數據,還有一種方案。就是方案四+方案三,只是不用取模后分組。
userId % 64 / count * count
因為上述公式,得出結果中,不一定每一片數據都是平均分布的。其實我們可以取模后,按范圍划分分片,如下公式。
第一片 0<userId % 64<15
第二片 16<userId % 64<31
第三片 32<userId % 64<47
第四片 48<userId % 64<63
當然范圍可以自定義,看取模后落入哪個值的數量比較多,就切某一片數據就好了,具體就不畫圖了,跟方案四類似。
因為遷移數據的原因,方案四中,如果數據量大,達到1000萬行記錄,每次遷移都需要遷移很多的數據,所以很多公司會盡早分庫分表。
但是在業務優先情況下,一直迭代業務,數據一進達到很多的情況下16分支一也是很多的數據時,我們就可以用一致性Hash理念--按范圍分庫
歡迎關注
我的公眾號 :地藏思維
掘金:地藏Kelvin
簡書:地藏Kelvin
CSDN:地藏Kelvin
我的Gitee: 地藏Kelvin https://gitee.com/dizang-kelvin