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