[NewLife.XCode]分表分庫(百億級大數據存儲)

  • 2019 年 10 月 3 日
  • 筆記

NewLife.XCode是一個有15年歷史的開源數據中間件,支援netcore/net45/net40,由新生命團隊(2002~2019)開發完成並維護至今,以下簡稱XCode。

整個系列教程會大量結合示例程式碼和運行日誌來進行深入分析,蘊含多年開發經驗於其中,代表作有百億級大數據實時計算項目。

開源地址:https://github.com/NewLifeX/X (求star, 938+)

 

XCode是重度充血模型,以單表操作為核心,不支援多表關聯Join,複雜查詢只能在where上做文章,整個select語句一定是from單表,因此對分表操作具有天然優勢!

!! 閱讀本文之前,建議回顧《百億級性能》,其中“索引完備”章節詳細描述了大型數據表的核心要點。

 

100億數據其實並不多,一個比較常見的數據分表分庫模型:

MySql資料庫8主8從,每伺服器8個庫,每個庫16張表,共1024張表(從庫也有1024張表) ,每張表1000萬到5000萬數據,整好100億到500億數據!

 

常式剖析 

常式位置:https://github.com/NewLifeX/X/tree/master/Samples/SplitTableOrDatabase 

新建控制台項目,nuget引用NewLife.XCode後,建立一個實體模型(修改Model.xml):

<Tables Version="9.12.7136.19046" NameSpace="STOD.Entity" ConnName="STOD" Output="" BaseClass="Entity" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xs:schemaLocation="http://www.newlifex.com https://raw.githubusercontent.com/NewLifeX/X/master/XCode/ModelSchema.xsd" xmlns="http://www.newlifex.com/ModelSchema.xsd">    <Table Name="History" Description="歷史">      <Columns>        <Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="編號" />        <Column Name="Category" DataType="String" Description="類別" />        <Column Name="Action" DataType="String" Description="操作" />        <Column Name="UserName" DataType="String" Description="用戶名" />        <Column Name="CreateUserID" DataType="Int32" Description="用戶編號" />        <Column Name="CreateIP" DataType="String" Description="IP地址" />        <Column Name="CreateTime" DataType="DateTime" Description="時間" />        <Column Name="Remark" DataType="String" Length="500" Description="詳細資訊" />      </Columns>      <Indexes>        <Index Columns="CreateTime" />      </Indexes>    </Table>  </Tables>

在Build.tt上右鍵運行自定義工具,生成實體類“歷史.cs”和“歷史.Biz.cs”。不用修改其中程式碼,待會我們將藉助該實體類來演示分表分庫用法。

為了方便,我們將使用SQLite資料庫,因此不需要配置任何資料庫連接,XCode檢測到沒有名為STOD的連接字元串時,將默認使用SQLite。

此外,也可以通過指定名為STOD的連接字元串,使用其它非SQLite資料庫。

 

按數字散列分表分庫

大量訂單、用戶等資訊,可採用crc16散列分表,我們把該實體數據拆分到4個庫共16張表裡面:

static void TestByNumber()  {      XTrace.WriteLine("按數字分表分庫");        // 預先準備好各個庫的連接字元串,動態增加,也可以在配置文件寫好      for (var i = 0; i < 4; i++)      {          var connName = $"HDB_{i + 1}";          DAL.AddConnStr(connName, $"data source=numberData\{connName}.db", null, "sqlite");          History.Meta.ConnName = connName;            // 每庫建立4張表。這一步不是必須的,首次讀寫數據時也會創建          //for (var j = 0; j < 4; j++)          //{          //    History.Meta.TableName = $"History_{j + 1}";            //    // 初始化數據表          //    History.Meta.Session.InitData();          //}      }        //!!! 寫入數據測試        // 4個庫      for (var i = 0; i < 4; i++)      {          var connName = $"HDB_{i + 1}";          History.Meta.ConnName = connName;            // 每庫4張表          for (var j = 0; j < 4; j++)          {              History.Meta.TableName = $"History_{j + 1}";                // 插入一批數據              var list = new List<History>();              for (var n = 0; n < 1000; n++)              {                  var entity = new History                  {                      Category = "交易",                      Action = "轉賬",                      CreateUserID = 1234,                      CreateTime = DateTime.Now,                      Remark = $"[{Rand.NextString(6)}]向[{Rand.NextString(6)}]轉賬[¥{Rand.Next(1_000_000) / 100d}]"                  };                    list.Add(entity);              }                // 批量插入。兩種寫法等價              //list.BatchInsert();              list.Insert(true);          }      }  }

通過 DAL.AddConnStr 動態向系統註冊連接字元串:

var connName = $”HDB_{i + 1}”;

DAL.AddConnStr(connName, $”data source=numberData\{connName}.db”, null, “sqlite”);

連接名必須唯一,且有規律,後面要用到。資料庫名最好也有一定規律。 

使用時通過Meta.ConnName指定後續操作的連接名,Meta.TableName指定後續操作的表名,本執行緒有效,不會幹涉其它執行緒。

var connName = $”HDB_{i + 1}”;
History.Meta.ConnName = connName;

History.Meta.TableName = $”History_{j + 1}”;

注意,ConnName/TableName改變後,將會一直維持該參數,直到修改為新的連接名和表名。

指定表名連接名後,即可在本執行緒內持續使用,後面使用批量插入技術,給每張表插入一批數據。

 

運行效果如下:

 

 

 

 

連接字元串指定的numberData目錄下,生成了4個資料庫,每個資料庫生成了4張表,每張表內插入1000行數據。

指定不存在的資料庫和數據表時,XCode的反向工程將會自動建表建庫,這是它獨有的功能。(因非同步操作,密集建表建庫時可能有一定幾率失敗,重試即可)

 

按時間序列分表分庫

日誌型的時間序列數據,特別適合分表分庫存儲,定型拆分模式是,每月一個庫每天一張表。

static void TestByDate()  {      XTrace.WriteLine("按時間分表分庫,每月一個庫,每天一張表");        // 預先準備好各個庫的連接字元串,動態增加,也可以在配置文件寫好      var start = DateTime.Today;      for (var i = 0; i < 12; i++)      {          var dt = new DateTime(start.Year, i + 1, 1);          var connName = $"HDB_{dt:yyMM}";          DAL.AddConnStr(connName, $"data source=timeData\{connName}.db", null, "sqlite");      }        // 每月一個庫,每天一張表      start = new DateTime(start.Year, 1, 1);      for (var i = 0; i < 365; i++)      {          var dt = start.AddDays(i);          History.Meta.ConnName = $"HDB_{dt:yyMM}";          History.Meta.TableName = $"History_{dt:yyMMdd}";            // 插入一批數據          var list = new List<History>();          for (var n = 0; n < 1000; n++)          {              var entity = new History              {                  Category = "交易",                  Action = "轉賬",                  CreateUserID = 1234,                  CreateTime = DateTime.Now,                  Remark = $"[{Rand.NextString(6)}]向[{Rand.NextString(6)}]轉賬[¥{Rand.Next(1_000_000) / 100d}]"              };                list.Add(entity);          }            // 批量插入。兩種寫法等價          //list.BatchInsert();          list.Insert(true);      }  }

時間序列分表看起來比數字散列更簡單一些,分表邏輯清晰明了。

 

 

 

 

 

 常式遍歷了今年的365天,在連接字元串指定的timeData目錄下,生成了12個月份資料庫,然後每個庫裡面按月生成數據表,每張表插入1000行模擬數據。

 

綜上,分表分庫其實就是在操作資料庫之前,預先設置好 Meta.ConnName/Meta.TableName,其它操作不變!

 

分表查詢

說到分表,許多人第一反應就是,怎麼做跨表查詢?

不好意思,不支援!

只能在多張表上各自查詢,如果系統設計不合理,甚至可能需要在所有表上進行查詢。

不建議做視圖union,那樣會無窮無盡,業務邏輯還是放在程式碼中為好,資料庫做好存儲與基礎計算。

 

分表查詢的用法與分表添刪改一樣:

static void SearchByDate()  {      // 預先準備好各個庫的連接字元串,動態增加,也可以在配置文件寫好      var start = DateTime.Today;      for (var i = 0; i < 12; i++)      {          var dt = new DateTime(start.Year, i + 1, 1);          var connName = $"HDB_{dt:yyMM}";          DAL.AddConnStr(connName, $"data source=timeData\{connName}.db", null, "sqlite");      }        // 隨機日期。批量操作      start = new DateTime(start.Year, 1, 1);      {          var dt = start.AddDays(Rand.Next(0, 365));          XTrace.WriteLine("查詢日期:{0}", dt);            History.Meta.ConnName = $"HDB_{dt:yyMM}";          History.Meta.TableName = $"History_{dt:yyMMdd}";            var list = History.FindAll();          XTrace.WriteLine("數據:{0}", list.Count);      }        // 隨機日期。個例操作      start = new DateTime(start.Year, 1, 1);      {          var dt = start.AddDays(Rand.Next(0, 365));          XTrace.WriteLine("查詢日期:{0}", dt);          var list = History.Meta.ProcessWithSplit(              $"HDB_{dt:yyMM}",              $"History_{dt:yyMMdd}",              () => History.FindAll());            XTrace.WriteLine("數據:{0}", list.Count);      }  }

 

仍然是通過設置 Meta.ConnName/Meta.TableName 來實現分表分庫。日誌輸出可以看到查找了哪個庫哪張表。

這裡多了一個 History.Meta.ProcessWithSplit  ,其實是快捷方法,在回調內使用連接名和表名,退出後復原。

 

分表分庫後,最容易犯下的錯誤,就是使用時忘了設置表名,在錯誤的表上查找數據,然後怎麼也查不到……

 

分表策略

根據這些年的經驗:

  • Oracle適合單表1000萬~1億行數據,要做分區
  • MySql適合單表1000萬~5000萬行數據,很少人用MySql分區

如果統一在應用層做拆分,資料庫只負責存儲,那麼上面的方案適用於各種資料庫。

同時,單表數據上限,就是大家常問的應該分為幾張表?在系統生命周期內(一般1~2年),確保拆分後的每張表數據總量在1000萬附近最佳。

根據《百億級性能》,常見分表策略如下:

  • 日誌型時間序列表,如果每月數據不足1000萬,則按月分表,否則按天分表。缺點是數據熱點極為明顯,適合熱表、冷表、歸檔表的梯隊架構,優點是批量寫入和抽取性能顯著;
  • 狀態表(訂單、用戶等),按Crc16哈希分表,以1000萬為準,決定分表數量,向上取整為2的指數倍(為了好算)。數據冷熱均勻,利於單行查詢更新,缺點是不利於批量寫入和抽取;
  • 混合分表。訂單表可以根據單號Crc16哈希分表,便於單行查找更新,作為寬表擁有各種明細欄位,同時還可以基於訂單時間建立一套時間序列表,作為冗餘,只存儲單號等必要欄位。這樣就解決了又要主鍵分表,又要按時間維度查詢的問題。缺點就是訂單數據需要寫兩份,當然,時間序列表只需要插入單號,其它更新操作不涉及。

至於是否需要分庫,主要由存儲空間以及性能要求決定。

 

分表與分區對比

還有一個很常見的問題,為什麼使用分表而不是分區?

大型資料庫Oracle、MSSQL、MySql都支援分區,前兩者較多使用分區,MySql則較多分表。

分區和分表並沒有本質的不同,兩者都是為了把海量數據按照一定的策略拆分存儲,以優化寫入和查詢。

  • 分區除了能建立子索引外,還可以建立全局索引,而分表不能建立全局索引;
  • 分區能跨區查詢,但非常非常慢,一不小心就掃描所有分區;
  • 分表架構,很容易做成分庫,支援輕易擴展到多台伺服器上去,分區只能要求資料庫伺服器更強更大;
  • 分區主要由DBA操作,分表主要由程式設計師控制;

 

 

!!!某項目使用XCode分表功能,已經過生產環境三年半考驗,日均新增4000萬~5000萬數據量,2億多次添刪改,總數據量數百億。

 

博文答疑

2019年9月9日晚上19點,釘釘企業群“新生命團隊”,影片直播博文答疑。

今晚之後,如有問題,可以提問:https://github.com/NewLifeX/X/issues

 

 

 

系列教程

NewLife.XCode教程系列[2019版]

  1. 增刪改查入門。快速展現用法,程式碼配置連接字元串
  2. 數據模型文件。建立表格欄位和索引,名字以及數據類型規範,推薦欄位(時間,用戶,IP)
  3. 實體類詳解。數據類業務類,泛型基類,介面
  4. 功能設置。連接字元串,調試開關,SQL日誌,慢日誌,參數化,執行超時。程式碼與配置文件設置,連接字元串局部設置
  5. 反向工程。自動建立資料庫數據表
  6. 數據初始化。InitData寫入初始化數據
  7. 高級增刪改。重載攔截,自增欄位,Valid驗證,實體模型(時間,用戶,IP)
  8. 臟數據。如何產生,怎麼利用
  9. 增量累加。高並發統計
  10. 事務處理。單表和多表,不同連接,多種寫法
  11. 擴展屬性。多表關聯,Map映射
  12. 高級查詢。複雜條件,分頁,自定義擴展FieldItem,查總記錄數,查匯總統計
  13. 數據層快取。Sql快取,更新機制
  14. 實體快取。全表整理快取,更新機制
  15. 對象快取。字典快取,適用用戶等數據較多場景。
  16. 百億級性能。欄位精鍊,索引完備,合理查詢,充分利用快取
  17. 實體工廠。元數據,通用處理程式
  18. 角色許可權。Membership
  19. 導入導出。Xml,Json,二進位,網路或文件
  20. 分表分庫。常見拆分邏輯
  21. 高級統計。聚合統計,分組統計
  22. 批量寫入。批量插入,批量Upsert,非同步保存
  23. 實體隊列。寫入級快取,提升性能。
  24. 備份同步。備份數據,恢複數據,同步數據
  25. 數據服務。提供RPC介面服務,遠程執行查詢,例如SQLite網路版
  26. 大數據分析。ETL抽取,調度計算處理,結果持久化