.NET ORM 分表分庫【到底】怎麼做?
阿新 • • 發佈:2020-08-30
## 理論知識
分表 - 從表面意思上看呢,就是把一張表分成N多個小表,每一個小表都是完正的一張表。分表後資料都是存放在分表裡,總表只是一個外殼,存取資料發生在一個一個的分表裡面。分表後單表的併發能力提高了,磁碟I/O效能也提高了。併發能力為什麼提高了呢,因為查尋一次所花的時間變短了,如果出現高併發的話,總表可以根據不同 的查詢,將併發壓力分到不同的小表裡面。
分庫 - 把原本儲存於一個庫的資料分塊儲存到多個庫上,把原本儲存於一個表的資料分塊儲存到多個表上。資料庫中的資料量不一定是可控的,在未進行分表分庫的情況下,隨著時間和業務的發展,庫中的表會越來越多,表中的資料量也會越來越大,相應地,資料操作,增刪改查的開銷也會越來越大;另外,一臺伺服器的資源(CPU、磁碟、記憶體、IO等)是有限的,最終資料庫所能承載的資料量、資料處理能力都將遭遇瓶頸。
![](https://img2020.cnblogs.com/blog/31407/202008/31407-20200828032759646-1618203847.png)
## 情懷滿滿
分表、分庫在 .NET 下可謂是老大難題,簡單點可以使用類似 mycat 中介軟體,但是就 .NET 平臺的自身生態,很缺乏類似 sharding-jdbc 這樣強大的輪子。
本人就自身有限的技術水平和經驗,對分表、分庫進行分析,實現出自成一套的使用方法,雖然不極 sharding-jdbc 強大,但是還算比較通用、簡單。但願有朝一日出現一批真正 .NET 大神,造出偉大的開源專案,實現你我心中的抱負。
這套分表、分庫方法是建立在 .NET ORM FreeSql 之上做的,內容可能比較抽象,敬請諒解!後續會詳解各種租戶設計方案,除了按欄位區分租戶,還包括分庫、分表的方案,敬請關注!
![](https://img2020.cnblogs.com/blog/31407/202008/31407-20200828032759646-1618203847.png)
## 入戲準備
FreeSql 是 .Net ORM,能支援 .NetFramework4.0+、.NetCore、Xamarin、XAUI、Blazor、以及還有說不出來的執行平臺,因為程式碼綠色無依賴,支援新平臺非常簡單。目前單元測試數量:5000+,Nuget下載數量:180K+,原始碼幾乎每天都有提交。值得高興的是 FreeSql 加入了 ncc 開源社群:[https://github.com/dotnetcore/FreeSql](https://github.com/dotnetcore/FreeSql),加入組織之後社群責任感更大,需要更努力做好品質,為開源社群出一份力。
QQ群:4336577(已滿)、8578575(線上)、52508226(線上)
為什麼要重複造輪子?
![](https://img2020.cnblogs.com/blog/31407/202005/31407-20200525013907903-1470982538.png)
> FreeSql 主要優勢在於易用性上,基本是開箱即用,在不同資料庫之間切換相容性比較好。作者花了大量的時間精力在這個專案,肯請您花半小時瞭解下專案,謝謝。功能特性如下:
- 支援 CodeFirst 對比結構變化遷移;
- 支援 DbFirst 從資料庫匯入實體類;
- 支援 豐富的表示式函式,自定義解析;
- 支援 批量新增、批量更新、BulkCopy;
- 支援 導航屬性,貪婪載入、延時載入、級聯儲存;
- 支援 讀寫分離、分表分庫,租戶設計;
- 支援 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/達夢/神通/人大金倉/MsAccess;
FreeSql 使用非常簡單,【單機資料庫】只需要定義一個 IFreeSql 物件即可:
```csharp
static IFreeSql fsql = new FreeSql.FreeSqlBuilder()
.UseConnectionString(FreeSql.DataType.MySql, connectionString)
.UseAutoSyncStructure(true) //自動同步實體結構到資料庫
.Build(); //請務必定義成 Singleton 單例模式
```
![](https://img2020.cnblogs.com/blog/31407/202008/31407-20200828032759646-1618203847.png)
## 分表
既然是分表,那就大膽認為他是操作【單機資料庫】,只需要對實體類進行動態對映表名即可實現,FreeSql 原生用法、FreeSql.Repository 倉儲用法 都提供了 AsTable 方法對分表進行 CRUD 操作,例如:
```c#
var repo = fsql.GetRepository();
repo.AsTable(oldname => $"{oldname}_201903");
//對 Log_201903 表 CRUD
repo.Insert(new Log { ... });
repo.Update(...);
repo.Delete(...);
repo.Select...;
```
AsTable 動態設定實體對映的表名,達到對分表的操作目的。除了 CRUD 操作,還提供了建立分表的功能:
- 如果開啟了自動同步結構功能 UseAutoSyncStructure(true),則 AsTable 會自動建立對應分表;
- 可以使用 fsql.CodeFirst.SyncStructure(typeof(實體類), "分表名") 進行手工建表;
多數情況,我們都建議提前建立好分表,如果按月分表,手工建立一年的分表。
目前這種算是比較簡單入門的方案,遠不及 mycat、sharding-jdbc 那麼智慧,比如:
- 不能利用分表字段自動進行分表對映;
- 不能在查詢時根據 where 條件自動對映分表,甚至跨多個分表的聯合查詢;
![](https://img2020.cnblogs.com/blog/31407/202008/31407-20200828032759646-1618203847.png)
## 分庫(單機)
分庫,但是在同一個資料庫伺服器例項下。這種情況也可以使用 AsTable 方式進行操作,如下:
```c#
var repo = fsql.GetRepository();
repo.AsTable(oldname => $"{201903}.dbo.{oldname}");
//對 [201903].dbo.Log CRUD
```
分庫之後,老大難題是事務,如果使用 SqlServer 可以利用 TransactionScope 做簡單的跨庫事務,如下:
```c#
var repoLog = fsql.GetRepository();
var repoComment = fsql.GetRepository();
repoLog.AsTable(oldname => $"{201903}.dbo.{oldname}");
repoComment.AsTable(oldname => $"{201903}.dbo.{oldname}");
using (TransactionScope ts = new TransactionScope())
{
repoComment.Insert(new Comment { ... });
repoLog.Insert(new Log { ... });
ts.Complete();
}
```
![](https://img2020.cnblogs.com/blog/31407/202008/31407-20200828032759646-1618203847.png)
## 分庫(跨伺服器)
前面提到:【單機資料庫】只需要定義一個 IFreeSql 物件即可。那分庫是不是要定義很多個 IFreeSql 物件?答案是的。
一般思路可以定義 static ConcurrentDictionary 儲存所有 IFreeSql 物件(key = ConnectionString),當進行 CRUD 時獲取到對應的 IFreeSql 即可。由於 IFreeSql 是靜態單例設計長駐記憶體,分庫數量太多的時候會浪費資源,因為不是所有分庫都一直一直在訪問。例如租戶分庫 10000 個,定義 10000 個 static IFreeSql?
更好的辦法可以使用 IdleBus 空閒物件管理容器,有效組織物件重複利用,自動建立、銷燬,解決【例項】過多且長時間佔用的問題。有時候想做一個單例物件重複使用提升效能,但是定義多了,有的又可能一直空閒著佔用資源。專門解決:又想重複利用,又想少佔資源的場景。https://github.com/2881099/IdleBus
> dotnet add package IdleBus
```csharp
static IdleBus ib = new IdleBus(TimeSpan.FromMinutes(10));
ib.Register("db1", () => new FreeSqlBuilder().UseConnectionString(DataType.MySql, "str1").Build());
ib.Register("db2", () => new FreeSqlBuilder().UseConnectionString(DataType.MySql, "str2").Build());
ib.Register("db3", () => new FreeSqlBuilder().UseConnectionString(DataType.SqlServer, "str3").Build());
//...註冊很多個
ib.Get("db1").Select().Limit(10).ToList();
```
IdleBus 也是【單例】設計!主要的兩個方法,註冊,獲取。idlebus 註冊不是建立 IFreeSql,首次 Get 時才建立,後面會一直用已經建立的。還有一個超時機制,如果 10 分鐘該 IFreeSql 未使用會被 Dispose,然後下一次又會建立新的 IFreeSql,如此反覆。從而解決了 10000 個 IFreeSql 長駐記憶體的問題。
還利用 AsyncLocal 特性擴充套件使用起來更加方便:
```c#
public static class IdleBusExtesions
{
static AsyncLocal asyncDb = new AsyncLocal();
public static IdleBus ChangeDatabase(this IdleBus ib, string db)
{
asyncDb.Value = db;
return ib;
}
public static IFreeSql Get(this IdleBus ib) => ib.Get(asyncDb.Value ?? "db1");
public static IBaseRepository GetRepository(this IdleBus ib) where T : class
=> ib.Get().GetRepository();
}
```
- 使用 ChangeDatabase 切換 db;
- 使用 Get() 獲取當前 IFreeSql,省略每次都傳遞 db 引數;
- 使用 GetRepository 獲取當前 IFreeSql 對應的倉儲類;
> 注意:使用 IdleBus 需要弱化 IFreeSql 的存在,每次都使用 ib.Get 獲取 IFreeSql 物件;
```c#
IdleBus ib = ...; //單例注入
var fsql = ib.Get(); //獲取當前租戶對應的 IFreeSql
var fsql00102 = ib.ChangeDatabase("db2").Get(); //切換租戶,後面的操作都是針對 db2
var songRepository = ib.GetRepository();
var detailRepository = ib.GetRepository();
```
目前這種算是比較簡單入門的方案,遠不及 mycat、sharding-jdbc 那麼智慧,比如:沒有實現跨庫事務。
## 寫在最後
.NET 生態還處於較弱的狀態,呼籲大家支援、踴躍參與開源專案,為下一個 .NET 開源社群五年計劃做貢獻。
希望正在使用的、善良的您能動一動小手指,把文章轉發一下,讓更多人知道 .NET 有這樣一個好用的 ORM 存在。謝謝了!!
FreeSql 開源協議 MIT [https://github.com/dotnetcore/FreeSql](https://github.com/dotnetcore/FreeSql),可以商用,文件齊全。QQ群:4336577(已滿)、8578575(線上)、52508226(線上)
如果你有好的 ORM 實現想法,歡迎給作者留言討論,謝謝觀看!