數據分表小結


  • 背景
  • 分庫、分錶帶來的後遺症
  • 分表策略
  • 一些注意事項

背景

最近一段時間內結束了數據庫表拆分項目,這裏作個簡單的小結。node

本次拆分主要包括訂單和優惠券兩大塊,這兩塊都是覆蓋全集團全部分子公司全部業務線。隨着公司的業務飛速發展,無論是存儲的要求,仍是寫入、讀取的性都基本上到了警惕水位。mysql

訂單是交易的核心,優惠券是營銷的核心,這兩塊基本上是整個平臺的正向最核心部分。爲了支持將來三到五年的快速發展,咱們須要對數據進行拆分。算法

數據庫表拆分業內已經有不少成熟方案,已經不是什麼高深的技術,基本上是純工程化的流程,可是能有機會進行實際的操刀一把機會仍是可貴,因此很是有必要作個總結。spring

因爲分庫分表包含的技術選型和方式方法多種多樣,這篇文章不是羅列和彙總介紹各類方法,而是總結咱們在實施分庫分表過程當中的一些經驗。sql

根據業務場景判斷,咱們主要是作水平拆分,作邏輯 DB 拆分,考慮到將來數據庫寫入瓶頸能夠將一組 sharding 表直接遷移進分庫中。數據庫

分庫、分錶帶來的後遺症

分庫、分表會帶來不少的後遺症,會使整個系統架構變的複雜。分的好與很差最關鍵就是如何尋找那個 sharding key,若是這個 sharding key 恰好是業務維度上的分界線就會直接提高性能和改善複雜度,不然就會有各類腳手架來支撐,系統也就會變得複雜。緩存

好比訂單系統中的用戶__ID__、訂單__type__、商家__ID__、渠道__ID__,優惠券系統中的批次__ID__、渠道__ID__、機構__ID__ 等,這些都是潛在的 sharding keyspringboot

若是恰好有這麼一個 sharding key 存在後面處理路由(routing)就會很方便,不然就須要一些大而全的索引表來處理 OLAP 的查詢。session

一旦 sharding 以後首先要面對的問題就是查詢時排序分頁問題。mybatis

歸併排序

原來在一個數據庫表中處理排序分頁是比較方便的,sharding 以後就會存在多個數據源,這裏咱們將多個數據源統稱爲分片。

想要實現多分片排序分頁就須要將各個片的數據都聚集起來進行排序,就須要用到 歸併排序 算法。這些數據在各個分片中能夠作到有序的(輸出有序),可是總體上是無序的。

咱們看個簡單的例子:

shard node 1: {一、三、五、七、9}
shard node 2: {二、四、六、八、10}

這是作 奇偶 sharding 的兩個分片,咱們假設分頁參數設置爲每頁4條,當前第1頁,參數以下:

pageParameter:pageSize:四、currentPage:1

最樂觀狀況下咱們須要分別讀取兩個分片節點中的前兩條:

shard node 1: {一、3}
shard node 2: {二、4}

排序完恰好是 {一、二、三、4},可是這種場景基本上不太可能出現,假設以下分片節點數據:

shard node 1: {七、九、十一、1三、15}
shard node 2: {二、四、六、八、十、十二、14}

咱們仍是按照讀取每一個節點前兩條確定是錯誤的,由於最悲觀狀況下也是最真實的狀況就是排序完後全部的數據都來自一個分片。因此咱們須要讀取每一個節點的 pageSize 大小的數據出來纔有可能保證數據的正確性。

這個例子只是假設咱們的查詢條件輸出的數據恰好是均等的,真實的狀況必定是各類各樣的查詢條件篩選出來的數據集合,此時這個數據必定不是這樣的排列方式,最真實的就是最後者這種結構。

咱們以此類推,若是咱們的 currentPage:1000 那麼會出現什麼問題,咱們須要每一個 sharding node 讀取 __4000(1000*4=4000)__ 條數據出來排序,由於最悲觀狀況下有可能全部的數據均來自一個 sharding node

這樣無限制的翻頁下去,處理排序分頁的機器確定會內存撐爆,就算不撐爆必定會觸發性能瓶頸。

這個簡單的例子用來講明分片以後,排序分頁帶來的現實問題,這也有助於咱們理解分佈式系統在作多節點排序分頁時爲何有最大分頁限制。

深分頁性能問題-改變查詢條件從新分頁

一個龐大的數據集會經過多種方式進行數據拆分,按機構、按時間、按渠道等等,拆分在不一樣的數據源中。通常的深分頁問題咱們能夠經過改變查詢條件來平滑解決,可是這種方案並不能解決全部的業務場景。

好比,咱們有一個訂單列表,從C端用戶來查詢本身的訂單列表數據量不會很大,可是運營後臺系統可能面對全平臺的全部訂單數據量,因此數據量會很大。

改變查詢條件有兩種方式,一種是顯示的設置,儘可能縮小查詢範圍,這種設置通常都會優先考慮,好比時間範圍、支付狀態、配送狀態等等,經過多個疊加條件就能夠橫豎過濾出很小一部分數據集。

那麼第二種條件爲隱式設置。好比訂單列表一般是按照訂單建立時間來排序,那麼當翻頁到限制的條件時,咱們能夠改變這個時間。

sharding node 1:
orderID     createDateTime
100000      2018-01-10 10:10:10
200000      2018-01-10 10:10:11
300000      2018-01-10 10:10:12
400000      2018-01-10 10:10:13
500000      2018-01-20 10:10:10
600000      2018-01-20 10:10:11
700000      2018-01-20 10:10:12
sharding node 2:
orderID     createDateTime
110000      2018-01-11 10:10:10
220000      2018-01-11 10:10:11
320000      2018-01-11 10:10:12
420000      2018-01-11 10:10:13
520000      2018-01-21 10:10:10
620000      2018-01-21 10:10:11
720000      2018-01-21 10:10:12

咱們假設上面是一個訂單列表,orderID 訂單號你們就不要在乎順序性了。由於 sharding 以後全部的 orderID 都會由發號器統一發放,多個集羣多個消費者同時獲取,可是建立訂單的速度是不同的,因此順序性已經不存在了。

上面的兩個 sharding node 基本上訂單號是交叉的,若是按照時間排序 node 1node 2 是要交替獲取數據。

好比咱們的查詢條件和分頁參數:

where createDateTime>'2018-01-11 00:00:00'
pageParameter:pageSize:五、currentPage:1

獲取的結果集爲:

orderID     createDateTime
100000      2018-01-10 10:10:10
200000      2018-01-10 10:10:11
300000      2018-01-10 10:10:12
400000      2018-01-10 10:10:13
110000      2018-01-11 10:10:10

前面 4 條記錄來自 node 1 後面 1 條數據來自 node 2 ,整個排序集合爲:

sharding node 1:
orderID     createDateTime
100000      2018-01-10 10:10:10
200000      2018-01-10 10:10:11
300000      2018-01-10 10:10:12
400000      2018-01-10 10:10:13
500000      2018-01-20 10:10:10

sharding node 2:
orderID     createDateTime
110000      2018-01-11 10:10:10
220000      2018-01-11 10:10:11
320000      2018-01-11 10:10:12
420000      2018-01-11 10:10:13
520000      2018-01-21 10:10:10

按照這樣一直翻頁下去每翻頁一次就須要在 node 1 、node 2 多獲取 5 條數據。這裏咱們能夠經過修改查詢條件來讓整個翻頁變爲從新查詢。

where createDateTime>'2018-01-11 10:10:13'

由於咱們能夠肯定在 ‘2018-01-11 10:10:13’ 時間以前全部的數據都已經查詢過,可是爲何時間不是從 ‘2018-01-21 10:10:10’ 開始,由於咱們要考慮併發狀況,在 1s 內會有多個訂單進來。

這種方式是實現最簡單,不須要藉助外部的計算來支撐。這種方式有一個問題就是要想從新計算分頁的時候不丟失數據就須要保留原來一條數據,這樣才能知道開始的時間在哪裏,這樣就會在下次的分頁中看到這條時間。可是從真實的深分頁場景來看也能夠忽略,由於不多有人會一頁一頁一直到翻到500頁,而是直接跳到最後幾頁,這個時候就不存在那個問題。

若是非要精準控制這個誤差就須要記住區間,或者用其餘方式來實現了,好比全量查詢表、sharding 索引表、最大下單 tps 值之類的,用來輔助計算。

(能夠利用數據同步中間件創建單表多級索引、多表多維度索引來輔助計算。咱們使用到的數據同步中間件有 datax、yugong、otter、canal 能夠解決全量、增量同步問題)。

分表策略

分表有多種方式,modrangpresharding自定義路由,每種方式都有必定的側重。

咱們主要使用 mod + presharding 的方式,這種方式帶來的最大的一個問題就是後期的節點變更數據遷移問題,能夠經過參考一致性 hash 算法的虛擬節點來解決。

數據表拆分和 cache sharding 有一些區別,cache 能接受 cache miss ,經過被動緩存的方式能夠維護起 cache 數據。可是數據庫不存在 select miss 這種場景。

cache sharding 場景下一致性 hash 能夠用來消除減小、增長 sharding node 時相鄰分片壓力問題。 可是數據庫一旦出現數據遷移必定是不能接受數據查詢不出來的。因此咱們爲了未來數據的平滑遷移,作了一個 虛擬節點 + 真實節點 mapping

physics node : node 1 node 2 node 3 node 4
virtual node : node 1 node 2 node 3.....node 20
node mapping :
virtual node 1 ~ node 5 {physics node 1}
virtual node 6 ~ node 10 {physics node 2}
virtual node 11 ~ node 15 {physics node 3}
virtual node 16 ~ node 20 {physics node 4}

爲了減小未來遷移數據時 rehash 的成本和延遲的開銷,將 hash 後的值保存在表裏,未來遷移直接查詢出來快速導入。

hash 片 2 的次方問題

在咱們熟悉的 hashmap 裏,爲了減小衝突和提供必定的性能將 hash 桶的大小設置成 2 的 n 次方,而後採用 hash&(legnth-1) 位與的方式計算,這樣主要是大師們發現 2 的 n 次方的二進制除了高位是 0 以外全部地位都是 1,經過位與能夠快速反轉二進制而後地位加 1 就是最終的值。

咱們在作數據庫 sharding 的時候不須要參考這一原則,這一原則主要是爲了程序內部 hash 表使用,外部咱們原本就是要 hash mod 肯定 sharding node

經過 mod 取模的方式會出現不均勻問題,在此基礎上能夠作個 自定義奇偶路由,這樣能夠均勻兩邊的數據。

一些注意事項

1.在現有項目中集成 sharding-JDBC 有一些小問題,sharding-jdbc 不支持批量插入,若是項目中已經使用了大量的批量插入語句就須要改造,或者使用 輔助hash計算物理表名,在批量插入。

2.原有項目數據層使用 Druid + MyBatis,集成了 sharding-JDBC 以後 sharding-JDBC包裝了 Druid ,因此一些 sharding-JDBC 不支持的sql語句基本就過不去了。

3.使用 springboot 集成 sharding-JDBC 的時候,在bean加載的時候我須要設置 IncrementIdGenerator ,可是出現classloader問題。

IncrementIdGenerator incrementIdGenerator = this.getIncrementIdGenerator(dataSource);

ShardingRule shardingRule = shardingRuleConfiguration.build(dataSourceMap);
((IdGenerator) shardingRule.getDefaultKeyGenerator()).setIncrementIdGenerator(incrementIdGenerator);
private IncrementIdGenerator getIncrementIdGenerator(DataSource druidDataSource) {
...
    }

後來發現 springboot的類加載器使用的是 restartclassloader,因此致使轉換一直失敗。只要去掉 spring-boot-devtools package便可,restartclassloader 是爲了熱啓動。

4.dao.xml 逆向工程問題,咱們使用的不少數據庫表mybatis生成工具生成的時候都是物理表名,一旦咱們使用了sharding-JDCB以後都是用的邏輯表名,因此生成工具須要提供選項來設置邏輯表名。

5.爲 mybatis 提供的 SqlSessionFactory 須要在Druid的基礎上用shading-JDCB包裝下。

6.sharding-JDBC DefaultkeyGenerator 默認採用是 snowflake 算法,可是咱們不能直接用咱們須要根據 datacenterid-workerid 本身配合zookeeper來設置 workerId 段。
(snowflake workId 10 bit 十進制 1023,dataCenterId 5 bit 十進制 31 、WorkId 5 bit 十進制 31)

7.因爲咱們使用的是 mysql com.mysql.jdbc.ReplicationDriver 自帶的實現讀寫分離,因此處理讀寫分離會方便不少。若是不是使用的這種就須要手動設置 Datasource Hint 來處理。

8.在使用 mybatis dao mapper 的時候須要多份邏輯表,由於有些數據源數據表是不須要走sharding的,自定義shardingStragety 來處理分支邏輯。

9 全局id幾種方法
9.1 若是使用 zookeeper 來作分佈式ID,就要注意 session expired 可能會存在重複 workid 問題,加鎖或者接受必定程度的並行(有序列號保證一段時間空間)。

9.2.採用集中發號器服務,在主DB中採用預生成表+incrment 插件(經典取號器實現,innodb 存儲引擎中的 TRX_SYS_TRX_ID_STORE 事務號也是這種方式)

9.3.定長髮號器、業務規則發號器,這種須要業務上下文的發號器實現都須要預先配置,而後每次請求帶上獲取上下文來講明獲取業務類型

10.在項目中有些地方使用了自增id排序,數據表拆分以後就須要進行改造,由於ID大小順序已經不存在了。根據數據的最新排序時使用了id排序須要改形成用時間字段排序。

做者:王清培 (滬江集團資深JAVA架構師)

相關文章
相關標籤/搜索