1. 程式人生 > 其它 >.net core 基於Dapper 的分庫分表開源框架(core-data)

.net core 基於Dapper 的分庫分表開源框架(core-data)

一、前言

分享基於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