如何選擇合適的分表鍵、路由規則、分片數

1、分表鍵的選擇

什麼是分表鍵

分表鍵即分庫/分表字段,zebra裏面叫作維度,是在水平拆分過程當中用於生成拆分規則的數據表字段。Zebra 根據分表鍵的值將數據表水平拆分到每一個物理分庫中。數據庫

數據表拆分的首要原則,就是要儘量找到數據表中的數據在業務邏輯上的主體,並肯定大部分(或核心的)數據庫操做都是圍繞這個主體的數據進行,而後可以使用該主體對應的字段做爲分表鍵,進行分庫分表。併發

業務邏輯上的主體,一般與業務的應用場景相關,下面的一些典型應用場景都有明確的業務邏輯主體,可用於分表鍵:函數

  • 面向用戶的互聯網應用,都是圍繞用戶維度來作各類操做,那麼業務邏輯主體就是用戶,可以使用用戶對應的字段做爲分表鍵;                   
  • 側重於賣家的電商應用,都是圍繞賣家維度來進行各類操做,那麼業務邏輯主體就是賣家,可以使用賣家對應的字段做爲分表鍵;

以此類推,其它類型的應用場景,大多也能找到合適的業務邏輯主體做爲分表鍵的選擇。性能

若是確實找不到合適的業務邏輯主體做爲分表鍵,那麼能夠考慮下面的方法來選擇分表鍵:大數據

  • 根據數據分佈和訪問的均衡度來考慮分表鍵,儘可能將數據表中的數據相對均勻地分佈在不一樣的物理分庫/分表中,適用於大量分析型查詢的應用場景(查詢併發度大部分能維持爲1);
  • 按照數字(字符串)類型與時間類型字段相結合做爲分表鍵,進行分庫和分表,適用於日誌檢索類的應用場景。

注意:不管選擇什麼拆分鍵,採用何種拆分策略,都要注意拆分值是否存在熱點的問題,儘可能規避熱點數據來選擇拆分鍵。線程

注意:不必定須要拿數據庫主鍵當作分表鍵,也能夠拿其餘業務值當分表鍵。拿主鍵當分表鍵的好處是能夠散列均衡,減小熱點問題。日誌

多個分表鍵如何處理

大部分場景下,一張表的查詢條件比較單一,只須要一個分表鍵便可;可是有的時候,業務必需要有多個分表鍵,沒有辦法歸一成一個。此時通常有四種處理方式:code

名詞定義:orm

  • 主分表鍵=主維度,在主維度上,數據可以增刪改查;
  • 輔助分表鍵=輔維度,在輔助維度上,只能進行數據查詢

在主維度上全表掃描

因爲SQL中沒有主維度,因此在對輔助維度進行查詢時,只能在全部的主維度的表進行查詢一遍,而後聚合。目前zebra的併發粒度是在數據庫級別的,也就是說若是分了4個庫,32張表,最終會以4個線程去併發查詢32張表,最終把結果合併輸出。router

適用場景:輔助維度的查詢請求的量很小,而且是運營查詢,對性能要求不高

多維度數據進行冗餘同步

主維度的數據,經過binlog的方式,同步到輔助維度一份。那麼在查詢輔助維度時,會落到輔助維度的數據上進行查詢。

適用場景:輔助維度的查詢請求的量也很可觀,不能直接使用第一種全表掃描的方式

二維巧妙歸一維

輔助維度其實有的時候也是主維度,好比在訂單表Order中,OrderID和UserID實際上是一一對應的,Order表的主維度是UserID,OrderID是輔助維度,可是因爲OrderID其中的6位和UserID徹底一致,也就是說,在OrderID中會把UserID打進去。

在路由的時候,若是SQL中帶有UserID,那麼直接拿UserID進行Hash取模路由;若是SQL中帶有的OrderID維度,那麼取出OrderID中的6位UserID進行Hash取模路由,結果是一致的。

適用場景:輔助維度和主維度其實能夠經過將主維度和輔助維度的值進行信息共享

創建索引表

對於輔助維度能夠建一張輔助維度和主維度的映射表。

舉例來講,表A有兩個維度,主維度a,輔助維度b,目前只有主維度的一份數據。

此時,若是有SQL: select * from A where b = ?過來,那麼勢必會在主維度上進行全表掃描。

那麼建一張新表B_A_Index,裏面就只有兩個字段,a和b的值,這張表能夠分表,也能夠不分表,建議分表這張表的主維度就是b。

因此能夠先查:select  a  from B_A_Index where b = ?,得到到a的值,而後 查詢 select * from A where a = 查詢到的值 and b = ? 進行查詢。

試用場景:主副維度是一一對應的。優點是,無需數據冗餘,只須要冗餘一份索引數據。缺點是,須要業務進行略微的改造。

2、分片數的選擇

zebra 中的水平拆分有兩個層次:分庫和分表。

表數目決策

通常狀況下,建議單個物理分表的容量不超過1000萬行數據。一般能夠預估2到5年的數據增加量,用估算出的總數據量除以總的物理分庫數,再除以建議的最大數據量1000萬,便可得出每一個物理分庫上須要建立的物理分表數:

  • (將來3到5年內總共的記錄行數)  /  單張表建議記錄行數              (單張表建議記錄行數  =  1000萬)

表的數量不宜過多,涉及到聚合查詢或者分表鍵在多個表上的SQL語句,就會併發到更多的表上進行查詢。舉個例子,分了4個表和分了2個表兩種狀況,一種須要併發到4表上執行,一種只須要併發到2張表上執行,顯而後者效率更高。

表的數目不宜過少,少的壞處在於一旦容量不夠就又要擴容了,而分庫分表的庫想要擴容是比較麻煩的。通常建議一次分夠。

建議表的數目是2的冪次個數,方便將來可能的遷移。

庫數目決策

  • 計算公式:按照存儲容量來計算 = (3到5年內的存儲容量)/  單個庫建議存儲容量                    (單個庫建議存儲容量  <300G之內)

DBA的操做,通常狀況下,會把若干個分庫放到一臺實例上去。將來一旦容量不夠,要發生遷移,一般是對數據庫進行遷移。因此庫的數目纔是最終決定容量大小

最差狀況,全部的分庫都共享數據庫機器。最優狀況,每一個分庫都獨佔一臺數據庫機器。通常建議一個數據庫機器上存放8個數據庫分庫。

3、分表策略的選擇

分表方式 解釋 優勢 缺點 試用場景 版本要求
Hash 拿分表鍵的值Hash取模進行路由。最經常使用的分表方式。 數據量散列均衡,每一個表的數據量大體相同 請求壓力散列均衡,不存在訪問熱點 一旦現有的表數據量須要再次擴容時,須要涉及到數據移動,比較麻煩。因此通常建議是一次性分夠。 在線服務。通常均以UserID或者ShopID等進行hash。 任意版本
Range 拿分表鍵按照ID範圍進行路由,好比id在1-10000的在第一個表中,10001-20000的在第二個表中,依次類推。這種狀況下,分表鍵只能是數值類型。 1.數據量可控,能夠均衡,也能夠不均衡. 2.擴容比較方便,由於若是ID範圍不夠了,只須要調整規則,而後建好新表便可。 沒法解決熱點問題,若是某一段數據訪問QPS特別高,就會落到單表上進行操做。 離線服務。 2.9.4以上
時間 拿分表鍵按照時間範圍進行路由,好比時間在1月的在第一個表中,在2月的在第二個表中,依次類推。這種狀況下,分表鍵只能是時間類型。 擴容比較方便,由於若是時間範圍不夠了,只須要調整規則,而後建好新表便可 1.數據量不可控,有可能單表數據量特別大,有可能單表數據量特別小 2.沒法解決熱點問題,若是某一段數據訪問QPS特別高,就會落到單表上進行操做。 離線服務。好比線下運營使用的表、日誌表等等

1.按照UserID進行Hash分表,根據UserID進行查詢

XML格式

<?xml version="1.0" encoding="UTF-8"?>
<router-rule> 
<table-shard-rule table="Order" generatedPK="id"> 
 <shard-dimension dbRule="#UserID#.toInteger()%32" dbIndexes="order_test[0-31]" 
 tbRule="#UserID#.toInteger().intdiv(32)%32" tbSuffix="alldb:[0,1023]" 
 isMaster="true"> 
 </shard-dimension> 
 </table-shard-rule>
</router-rule>

Order表的UserID維度一共分了32個庫,分別是order_test0到order_test31。一共分了1024張表,表名分表是Order0到Order1023,平均分到了32個庫中,每一個庫32張表。

2.按照UserID進行Hash分表,根據UserID和ShopID進行查詢

XML格式

<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
 <table-shard-rule table="Order" generatedPK="OrderID">
 <shard-dimension dbRule="#UserID#.toInteger()%10000%32" dbIndexes="order_test[0-31]" 
 tbRule="(#UserID#.toInteger()%10000).intdiv(32) %32" tbSuffix="alldb:[0,1023]"
 isMaster="true">
 </shard-dimension>
 <shard-dimension dbRule="#ShopID# == null ? SKIP : (#ShopID#.toInteger()%16)" dbIndexes="order_shop_test[0-15]" 
 tbRule="(#ShopID#.toInteger()).intdiv(16) %16" tbSuffix="alldb:[0,255]"
 needSync="true"
 isMaster="false">
 </shard-dimension>
 </table-shard-rule>
</router-rule>

Order表的ShopID維度的數據是根據UserID的數據從新經過binlog同步了一份。該份數據分了16個庫,分別是order_shop_test0到order_shop_test15。一共分了256張表,每一個庫16張表,表名分表是Order0到Order255。

其中zebra會負責根據該配置,把UserID維度的數據自動的同步到ShopID維度,背後是經過binlog方式,因此會有needSync的屬性。**可是,該功能目前已經永久性停用,若是須要使用,請自行接入DTS/DataBus。

3.根據UserID進行Hash分表,根據UserID和OrderID進行查詢

<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
 <table-shard-rule table="Order" generatedPK="OrderID">
 <shard-dimension dbRule="#UserID#.toInteger()%10000%32" dbIndexes="order_test[0-31]" 
 tbRule="(#UserID#.toInteger()%10000).intdiv(32) %32" tbSuffix="alldb:[0,1023]"
 isMaster="true">
 </shard-dimension>
 <shard-dimension dbRule="#OrderID#[13..16].toInteger() % 32" dbIndexes="order_test[0-31]" 
 tbRule="(#OrderID#[13..16].toInteger()).intdiv(32) %32" tbSuffix="alldb:[0,1023]"
 isMaster="false">
 </shard-dimension>
 </table-shard-rule>
</router-rule>

Order表的OrderID維度的數據和UserID的數據是一致的,並無冗餘。可是因爲OrderID中的13到16位就是UserID,因此可使用UserID的數據進行查詢。本質上,OrderID和UserID確定能一一對應,實際上是一個維度。

4.根據UserID進行Hash分表,根據AddTime彙總大表供線下運營查詢

<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
 <table-shard-rule table="Order" generatedPK="OrderID">
 <shard-dimension dbRule="#UserID#.toInteger()%10000%32" dbIndexes="order_test[0-31]" 
 tbRule="(#UserID#.toInteger()%10000).intdiv(32) %32" tbSuffix="alldb:[0,1023]"
 isMaster="true">
 </shard-dimension>
 <shard-dimension dbRule="#AddTime# == null ? SKIP : 0" dbIndexes="order_one" 
 tbRule="#AddTime# == null ? SKIP : 0" tbSuffix="alldb:[]"
 needSync="true"
 isMaster="false">
 </shard-dimension>
 </table-shard-rule>
</router-rule>

Order表的AddTime維度,實際上是經過binlog的方式把UserID的數據彙總到了order_one這個庫,表名爲Order這個表。這個彙總的過程是自動的。needSync=true。

線上服務使用ShardDataSource,對UserID的數據進行增刪改查。運營服務在訪問運營庫order_one時,無需使用ShardDataSource,直接使用GroupDataSource進行訪問,僅作查詢使用。

5.根據UserID的crc32值進行Hash分表

<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
 <table-shard-rule table="Order" generatedPK="OrderID">
 <shard-dimension dbRule="crc32(#UserID#.toInteger())%10000%32" dbIndexes="order_test[0-31]" 
 tbRule="(crc32(#UserID#.toInteger())%10000).intdiv(32) %32" tbSuffix="alldb:[0,1023]"
 isMaster="true">
 </shard-dimension>
 </table-shard-rule>
</router-rule>

crc32是目前zebra的一個內置函數。

內置HASH函數

  • crc32(Object value)
  • crc32(Object value,String encode)
  • md5(String value)

6.主維度根據UserID進行Hash分表,輔助維度根據AddTime進行時間分表

<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
 <table-shard-rule table="Order" generatedPK="OrderID">
 <shard-dimension dbRule="#UserID#.toInteger()%10000%32" dbIndexes="order_test[0-31]" 
 tbRule="(#UserID#.toInteger()%10000).intdiv(32) %32" tbSuffix="alldb:[0,1023]"
 isMaster="true">
 </shard-dimension>
 <shard-dimension dbRule="shardByMonth(#AddTime#,"yyyy-MM-dd", "2017-01-01","2027-01-31",3,0) % 4" dbIndexes="order_time[0-3]" 
 tbRule="shardByMonth(#AddTime#,"yyyy-MM-dd", "2017-01-01","2027-01-31",3,0).intdiv(4)% 10" tbSuffix="alldb:[0,39]"
 needSync="true"
 isMaster="false">
 </shard-dimension>
 </table-shard-rule>
</router-rule>

Order表的AddTime維度,實際上是經過binlog的方式把UserID的數據從新根據時間進行分表的,上述例子中,一共分了4個庫,40張表,3個月一張表,涵蓋10年的量。

線上系統直接使用ShardDataSource使用UserID維度,線下運營系統也是用ShardDataSource,若是帶了AddTime,都會落到AddTime維度的數據上進行查詢。支持>,<,>=,<=,between and等範圍查詢;不支持Join。

內置時間函數

  • shardByMonth("#CreateTime",String timeFormat, String startTime, String end, int monthPerTable, int defaultTableIndex)該函數是根據月份進行計算路由的。其中,startTime和endTime代表起始時間和結束時間,monthPerTable是指從起始時間開始幾個月一張表,defaultTableIndex是指若是超出了起始時間和結束時間時到一張默認的表的Index。
相關文章
相關標籤/搜索