數據分表小結

来源:https://www.cnblogs.com/wangiqngpei557/archive/2018/07/21/9347232.html
-Advertisement-
Play Games

最近一段時間內結束了資料庫表拆分項目,這裡做個簡單的小結。 本次拆分主要包括訂單和優惠券兩大塊,這兩塊都是覆蓋全集團所有分子公司所有業務線。隨著公司的業務飛速發展,不管是存儲的要求,還是寫入、讀取的性都基本上到了警戒水位。 訂單是交易的核心,優惠券是營銷的核心,這兩塊基本上是整個平臺的正向最核... ...



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

背景

最近一段時間內結束了資料庫表拆分項目,這裡做個簡單的小結。

本次拆分主要包括訂單和優惠券兩大塊,這兩塊都是覆蓋全集團所有分子公司所有業務線。隨著公司的業務飛速發展,不管是存儲的要求,還是寫入、讀取的性都基本上到了警戒水位。

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

資料庫表拆分業內已經有很多成熟方案,已經不是什麼高深的技術,基本上是純工程化的流程,但是能有機會進行實際的操刀一把機會還是難得,所以非常有必要做個總結。

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

根據業務場景判斷,我們主要是做水平拆分,做邏輯 DB 拆分,考慮到未來資料庫寫入瓶頸可以將一組 sharding 表直接遷移進分庫中。

分庫、分錶帶來的後遺症

分庫、分表會帶來很多的後遺症,會使整個系統架構變的複雜。分的好與不好最關鍵就是如何尋找那個 sharding key,如果這個 sharding key 剛好是業務維度上的分界線就會直接提升性能和改善複雜度,否則就會有各種腳手架來支撐,系統也就會變得複雜。

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

如果剛好有這麼一個 sharding key 存在後面處理路由(routing)就會很方便,否則就需要一些大而全的索引表來處理 OLAP 的查詢。

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

歸併排序

原來在一個資料庫表中處理排序分頁是比較方便的,sharding 之後就會存在多個數據源,這裡我們將多個數據源統稱為分片。

想要實現多分片排序分頁就需要將各個片的數據都彙集起來進行排序,就需要用到 歸併排序 演算法。這些數據在各個分片中可以做到有序的(輸出有序),但是整體上是無序的。

我們看個簡單的例子:

shard node 1: {1、3、5、7、9}
shard node 2: {2、4、6、8、10}

這是做 奇偶 sharding 的兩個分片,我們假設分頁參數設置為每頁4條,當前第1頁,參數如下:

pageParameter:pageSize:4、currentPage:1

最樂觀情況下我們需要分別讀取兩個分片節點中的前兩條:

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

排序完剛好是 {1、2、3、4},但是這種場景基本上不太可能出現,假設如下分片節點數據:

shard node 1: {7、9、11、13、15}
shard node 2: {2、4、6、8、10、12、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:5、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架構師)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 揭秘黑客工具教程:微信密碼怎麼破解?如何破解別人的微信密碼? 微信的出現確實給人們的生活帶來了極大的便利,現在人們在生活中不僅使用微信社交,還會在閑來無事時視頻,發一下紅包或者是轉賬。據不完全統計,目前微信在全國共有十億人次在使用,大家在使用的過程中均對其強大的功能予以稱贊。隨著微信功能日益強大,微 ...
  • 1、JSONString轉換為字典 2、JSONString轉換為數組 3、字典轉換為JSONString ...
  • TypeScript在node項目中的實踐 TypeScript可以理解為是JavaScript的一個超集,也就是說涵蓋了所有JavaScript的功能,併在之上有著自己獨特的語法。最近的一個新項目開始了TS的踩坑之旅,現分享一些可以借鑒的套路給大家。 為什麼選擇TS 作為巨硬公司出品的一個靜態強類 ...
  • URLSearchParams 介面定義了一些實用的方法來處理 URL 的查詢字元串。 URLSearchParams()是個構造函數,將返回一個可以操作查詢字元串的對象。 常用方法: 1、構造查詢字元串 2、獲取查詢字元串參數 相容性: 相容性較差 建議使用polyfill:https://git ...
  • 條件指令 所謂條件指令是指滿足某個條件時執行哪部分代碼,不滿足條件時執行哪部分條件代碼。vue條件指令有v-if,v-else-if,v-else三個,v-if條件渲染用來指示元素是否移除或者插入,根據表達式的值的真假條件渲染元素。 v-if示例 方式一使用v-if顯示標簽,當初始化值為true時, ...
  • 如果你是一個人在自學前端開發,或者是對前端開發有比較濃厚的興趣正想踏入前端領域,只要你在前端自學路上遇到了自己無法解決的技術難題,那麼儘管將你的疑惑交給我的小伙伴兒們吧,我們都是一群在前端自學路上摸爬滾打的有志青年,希望你可以來和我們共同交流。同時也希望你能獻出自己的一份力,幫助我的小伙伴兒們解決他 ...
  • JavaScript中數組有各種操作方法,以下通過舉例來說明各種方法的使用: 數組操作方法 push 在數組最後添加一個元素 pop 把數組最後一位取出來,並返回,且原來數組發生變化 shift 把數組第一位取出來,並返回,且原來數組發生變化 unshift 在數組第一位新增一個元素 join 把數 ...
  • 所謂meta標記就是用來描述一個HTML網頁文檔的屬性,也稱為元信息,這些信息並不會顯示在瀏覽器的頁面中,例如作者、日期和時間、網頁描述、頁面刷新等。 基本語法: <meta name = " "content=" "> <meta http-equiv=" " content = " "> nam ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...