在資料庫的數據日積月累的積累下,業務資料庫中的單表數據想必也越來越大,大到百萬、千萬、甚至上億級別的數據,這個時候就很有必要進行資料庫讀寫分離、以及單表分多表進行存儲,提高性能 ...
一、前言
感覺很久沒寫文章了,最近也比較忙,寫的相對比較少,抽空分享基於Dapper
的分庫分表開源框架core-data
的強大功能,更好的提高開發過程中的效率;
在資料庫的數據日積月累的積累下,業務資料庫中的單表數據想必也越來越大,大到百萬、千萬、甚至上億級別的數據,這個時候就很有必要進行資料庫讀寫分離、以及單表分多表進行存儲,提高性能,但是呢很多人不知道怎麼去分庫分表,也沒有現成的分庫分表的成熟框架,故不知道怎麼下手,又怕影響到業務;現在我給大家推薦core-data
的分庫分表開源框架。框架開源地址:https://github.com/overtly/core-data
二、基礎
2.1 回顧
這裡先來回顧下我上一篇文章中的技術棧路線圖,如下:
今天從這張技術棧圖中來詳細分享一切的基礎資料庫底層操作ORM。
2.2 core-data主要優勢:
上一篇文章.Net 微服務架構技術棧的那些事 中簡單的介紹了core-data
主要優勢,如下:
- 官方建議使用DDD 領域驅動設計思想開發
- 支持多種資料庫(MySql / SqlServer / SQLite ),簡單配置添加鏈接的配置即可
- 支持分表操作,自定義分表策略的支持
- 支持表達式方式編寫,減少寫Sql語句機械性工作
- 可對Dapper 進行擴展
- 性能依賴於Dapper 本身的性能,Dapper 本身是輕量級ORM ,官方測試性能都強於其他的ORM
- 框架支持Framework4.6 - NetStandard 2.0
三、實戰詳解
這裡都僅僅分享核心的內容代碼,不把整個代碼貼出來,有需要完整Demo源代碼請訪問 https://github.com/a312586670/NetCoreDemo
在我的解決方案的項目中 引用overt.core.data
nuget包,如下圖:
3.1 單表模式
創建用戶實體代碼如下:
/// <summary>
/// 標註資料庫對應的表名
/// </summary>
[Table("User")]
public class UserEntity
{
/// <summary>
/// 主鍵ID,標註自增ID
/// </summary>
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
/// <summary>
/// 商戶ID
/// </summary>
public int MerchantId { set; get; }
/// <summary>
/// 用戶名
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 真實姓名
/// </summary>
public string RealName { get; set; }
/// <summary>
/// 密碼
/// </summary>
public string Password { get; set; }
/// <summary>
/// 添加時間
/// </summary>
public DateTime AddTime { get; set; }
}
代碼中通過[Table("User")]
來和資料庫表進行映射關聯;
通過[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] 標註自增主鍵.
3.2 預設分表策略
從單表模式改成分表模式,並且按照商戶的模式進行分表,代碼實體代碼改造如下:
/// <summary>
/// 標註資料庫對於的表名
/// </summary>
[Table("User")]
public class UserEntity
{
/// <summary>
/// 主鍵ID,標註自增ID
/// </summary>
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
/// <summary>
/// 商戶ID
/// </summary>
[Submeter(Bit =2)]
public int MerchantId { set; get; }
/// <summary>
/// 用戶名
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 真實姓名
/// </summary>
public string RealName { get; set; }
/// <summary>
/// 密碼
/// </summary>
public string Password { get; set; }
/// <summary>
/// 添加時間
/// </summary>
public DateTime AddTime { get; set; }
}
代碼中 MerchantId 欄位上添加了[Submeter(Bit =2)]
標註,並且指定了Bit=2,將會分成根據MerchantId欄位取二進位進行md5 hash 取前兩位分成256張表
分表模式源碼分析
分表模式可以通過在欄位上標註Submeter
屬性,我們先來看看框架對於這個標註的源代碼,源代碼如下:
/// <summary>
/// 分表標識
/// </summary>
public class SubmeterAttribute : Attribute
{
/// <summary>
/// 16進位位數
/// 1 16
/// 2 256
/// 3 4096
/// ...
/// </summary>
public int Bit { get; set; }
}
開源框架中其中一個獲得分表的表名稱的擴展方法,僅僅只貼了一個擴展方法,有興趣的可以下載開源框架進行源碼閱讀。
/// <summary>
/// 獲取表名
/// </summary>
/// <param name="entity">實體實例</param>
/// <param name="tableNameFunc"></param>
/// <returns></returns>
public static string GetTableName<TEntity>(this TEntity entity, Func<string> tableNameFunc = null) where TEntity : class, new()
{
if (tableNameFunc != null)
return tableNameFunc.Invoke();
var t = typeof(TEntity);
var mTableName = t.GetMainTableName();
var propertyInfo = t.GetProperty<SubmeterAttribute>();
if (propertyInfo == null) // 代表沒有分表特性
return mTableName;
// 獲取分表
var suffix = propertyInfo.GetSuffix(entity);
return $"{mTableName}_{suffix}";
}
/// <summary>
/// 獲取尾碼(預設根據SubmeterAttribute 標註的位數進行Md5 hash 進行分表)
/// </summary>
/// <param name="val"></param>
/// <param name="bit"></param>
/// <returns></returns>
internal static string GetSuffix(string val, int bit = 2)
{
if (string.IsNullOrEmpty(val))
throw new ArgumentNullException($"分表數據為空");
if (bit <= 0)
throw new ArgumentOutOfRangeException("length", "length必須是大於零的值。");
var result = Encoding.Default.GetBytes(val.ToString()); //tbPass為輸入密碼的文本框
var md5Provider = new MD5CryptoServiceProvider();
var output = md5Provider.ComputeHash(result);
var hash = BitConverter.ToString(output).Replace("-", ""); //tbMd5pass為輸出加密文本
var suffix = hash.Substring(0, bit).ToUpper();
return suffix;
}
源代碼中通過SubmeterAttribute
特性進行對欄位進行標註分表,可以傳對應的bit參數進行框架預設的分表策略進行分表,但是很多情況下我們需要自定義分表策略,那我們應該怎麼去自定義分表策略呢?我們先等一下來實踐自定義分表策略,先來創建用戶的Repository
,代碼如下
IUserRepository
:
public interface IUserRepository : IBaseRepository<UserEntity>
{
}
需要繼承IBaseRepository<T>
的介面,該介面預設實現了基本的方法,開源框架中IBaseRepository<T>
代碼方法如下圖:
創建完IUserRepository
介面後,我們來創建它的實現UserRepository
,代碼如下:
public class UserRepository : BaseRepository<UserEntity>, IUserRepository
{
public UserRepository(IConfiguration configuration)
: base(configuration, "user")
{
}
}
從代碼中UserRepository
類繼承了BaseRepository<T>
類,我們來看看這個abstract類的基本結構,如下圖:
開源框架中BaseRepository<T>
抽象類繼承了PropertyAssist
類,我們再來看看它的有哪些方法,如下圖:
從圖中可以看到定義了一系列的virtual
方法,那既然是virtual
方法我們就可以進行重寫
CreateScriptFunc
:自動創建腳本數據表方法TableNameFunc
:可以進行自定義分表策略
3.3 自定義分表策略
我們來實現上面提出的自定義分表策略問題(根據商戶Id來進行分表,並且自動把不存在的表進行初始化創建),代碼改造如下:
IUserRepository
:
public interface IUserRepository : IBaseRepository<UserEntity>
{
int MerchantId { set; get; }
}
UserRepository
代碼改造如下:
public class UserRepository : BaseRepository<UserEntity>, IUserRepository
{
public UserRepository(IConfiguration configuration)
: base(configuration, "user")
{
}
/// <summary>
/// 用於根據商戶ID來進行分表
/// </summary>
public int MerchantId { set; get; }
//自定義分表策略
public override Func<string> TableNameFunc => () =>
{
var tableName = $"{GetMainTableName()}_{MerchantId}";
return tableName;
};
//自動創建分表的腳本
public override Func<string, string> CreateScriptFunc => (tableName) =>
{
//MySql
return "CREATE TABLE `" + tableName + "` (" +
" `UserId` int(11) NOT NULL AUTO_INCREMENT," +
" `UserName` varchar(200) DEFAULT NULL," +
" `Password` varchar(200) DEFAULT NULL," +
" `RealName` varchar(200) DEFAULT NULL," +
" `AddTime` datetime DEFAULT NULL," +
" `MerchantId` int(11) NOT NULL," +
" PRIMARY KEY(`UserId`)" +
") ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4; ";
};
}
3.4 資料庫讀寫分離
我們再來看看開源框架的基類代碼結構截圖,如下:
對於查詢的基本常用的方法都有一個isMaster=false
的參數,該參數就是用於是否讀取主庫,用於基本的主從資料庫的分離,也就是讀寫分離,那改怎麼配置讀寫分離資料庫呢
鏈接字元串如下圖:
分別指定了主從資料庫的鏈接字元串.
我們來分析源代碼,核心框架源代碼如下:
/// <summary>
/// 連接配置信息獲取
/// 1. master / secondary
/// 2. xx.master / xx.secondary
/// </summary>
public class DataSettings
{
#region Static Private Members
static readonly string _connNmeOfMaster = "master";
static readonly string _connNameOfSecondary = "secondary";
static readonly string _connNameOfPoint = ".";
static string _connNameOfPrefix = "";
#endregion
#region Private Member
/// <summary>
/// 主庫
/// </summary>
private string Master
{
get { return $"{_connNameOfPrefix}{_connNmeOfMaster}"; }
}
/// <summary>
/// 從庫
/// </summary>
private string Secondary
{
get
{
return $"{_connNameOfPrefix}{_connNameOfSecondary}";
}
}
#endregion
/// <summary>
/// 獲取鏈接名稱
/// </summary>
/// <param name="isMaster"></param>
/// <param name="store">不能包含點</param>
/// <returns></returns>
private string Key(bool isMaster = false, string store = "")
{
_connNameOfPrefix = string.IsNullOrEmpty(store) ? "" : $"{store}{_connNameOfPoint}";
var connName = string.Empty;
if (isMaster)
connName = Master;
else
connName = Secondary;
return connName;
}
}
代碼中根據isMaster 參數來進行讀寫資料庫鏈接參數的獲取,以達到讀寫分離的功能,同時還支持首碼的配置支持,也開源自由配置多個資料庫進行讀取,只需要構造函數中獲取配置即可。
上面的分表Demo 單元測試運行後的結果例子如下圖:
已經按照MerchantId 欄位進行分表
三、總結
到這裡用戶表已經根據商戶ID進行分表存儲了,這樣就做到了讀寫分離及自定義分表策略存儲數據,core-data
開源框架還支持更多的強大功能,實現了一系列的基礎CRUD的方法,不用寫任何的sql語句,Where表達式的支持,同時可以自定義複雜的sql語句,更多請訪問框架開源地址:https://github.com/overtly/core-data.
完整的Demo 代碼 已經放在github上,Demo代碼結構圖如下:
地址:https://github.com/a312586670/NetCoreDemo