efcore分表下"完美"實現

薛家明發表於2021-08-23

ShardingCore 如何呈現“完美”分表

  這篇文章是我針對efcore的分表的簡單介紹,如果您有以下需求那麼可以自己選擇是否使用本框架,本框架將一直持續更新下去,並且免費開源為.net生態做貢獻,如果您覺得不錯那麼請幫忙點個star謝謝,框架地址[`sharding-core`](https://github.com/xuejmnet/sharding-core) 您的支援是對我最大的動力。

如果您對分表有以下痛點那麼不妨試試我這邊開源的框架sharding-core ,是否需要無感知使用分表元件,是否需要支援abp,是否需要支援自定義分表規則,是否需要支援自定義分表鍵,是否需要支援特定的efcore版本,是否希望框架不帶任何三方框架乾淨,是否需要支援讀寫分離,是否需要動態新增表,是否需要支援join,group等操作,是否需要支援追蹤特性,是否想在不修改原先程式碼的基礎上擴充套件分表功能,如果一起上幾個條件任意組合且你在市面上沒辦法找到可替代的框架可以試試本框架。如何使用程式碼具體可以參考github 將程式碼下載下來如果本地裝了sqlserver直接執行單元測試或者Sample.SqlServer程式會自動在本地新建資料庫新建資料庫表結構,目前初始化資料為使用者資訊和使用者對應的月薪資訊表,使用者表以使用者id取模,使用者月薪表以月份分表。

首先需要了解本框架的一個版本號不然將對您的使用產生一定的分期,目前框架分為3個版本分別是2.x,3.x,5.x3個版本,分別對應efcore 2.x efcore 3.x efcore 5.x,有人要問為什麼不支援6.x呢(小弟剛剛在上週完成對本框架的開發重構,目前還未對efcore 6.x進行著手不過將在不遠的將來即將支援(目測1-2個星期內))。

目前efcore生態下有著許許多多的分表、分庫的解決方案,但是目前來講都有其不足點,比如需要手動設定分表字尾、需要大量替換現有程式碼、不支援事務等等一系列問題,所以在這個大前提下我之前開源了sharding-core 分表元件,這個分表元件是目前來說個人認為比較“完美”的分表元件,這個分表元件目前是參考了sharding-jdbc來實現的,但是比sharding-jdbc更加強大(因為C#的表示式)。首先我們來看下目前市面上有的分表元件的缺點我們來針對其缺點進行痛點解決。

efcore支援情況

efcore版本 是否支援
2.x 支援
3.x 支援
5.x 支援
6.x 即將支援

資料庫支援情況

資料庫 理論是否支援
SqlServer 支援
MySql 支援
PostgreSql 支援
SQLite 支援
Oracle 支援
其他 支援(只要efcore支援)

理論上只要是efcore對應版本支援的資料庫,sharding-core都將支援。

如何開始使用

1.建立一個資料庫物件繼承IShardingTable並且在對應的分表欄位上進行[ShardingTableKey]特性的標註

 /// <summary>
    /// 使用者表
    /// </summary>
    public class SysUserMod : IShardingTable
    {
        /// <summary>
        /// 使用者Id用於分表
        /// </summary>
        [ShardingTableKey(TailPrefix = "_")]
        public string Id { get; set; }
        /// <summary>
        /// 使用者名稱稱
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 使用者姓名
        /// </summary>
        public int Age { get; set; }
    }

2.建立對應的實體表對應配置 推薦 fluent api

    public class SysTestMap:IEntityTypeConfiguration<SysTest>
    {
        public void Configure(EntityTypeBuilder<SysTest> builder)
        {
            builder.HasKey(o => o.Id);
            builder.Property(o => o.Id).IsRequired().HasMaxLength(128);
            builder.Property(o => o.UserId).IsRequired().HasMaxLength(128);
            builder.ToTable(nameof(SysTest));
        }
    }

3.建立對應的分表規則 取模分表,引數2代表字尾2位就是00-99最多100張表,3表示模3== key.hashcode() %3

    public class SysUserModVirtualTableRoute : AbstractSimpleShardingModKeyStringVirtualTableRoute<SysUserMod>
    {
        public SysUserModVirtualTableRoute() : base(2,3)
        {
        }
    }

4建立對應執行的dbcontext 這一步除了繼承IShardingTableDbContext外其他和普通dbcontext一樣


    public class DefaultTableDbContext: DbContext,IShardingTableDbContext
    {
        public DefaultTableDbContext(DbContextOptions<DefaultTableDbContext> options) :base(options)
        {
            
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.ApplyConfiguration(new SysUserModMap());
        }

        public IRouteTail RouteTail { get; set; }
    }

5.新增分表dbcontext


    public class DefaultShardingDbContext:AbstractShardingDbContext<DefaultTableDbContext>
    {
        public DefaultShardingDbContext(DbContextOptions<DefaultShardingDbContext> options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.ApplyConfiguration(new SysUserModMap());
        }

        public override Type ShardingDbContextType => this.GetType();
    }

6.新增配置

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
			//原先的dbcontext可以用也可以不用如果原先的dbcontext還在用就繼續
            //services.AddDbContext<DefaultTableDbContext>(o => o.UseSqlServer("Data Source=localhost;Initial Catalog=ShardingCoreDBxx3;Integrated Security=True"));
            services.AddShardingDbContext<DefaultShardingDbContext, DefaultTableDbContext>(
                o => o.UseSqlServer("Data Source=localhost;Initial Catalog=ShardingCoreDBxx2;Integrated Security=True;")
                , op =>
                 {
                     op.EnsureCreatedWithOutShardingTable = true;
                     op.CreateShardingTableOnStart = true;
                     op.UseShardingOptionsBuilder(
                         (connection, builder) => builder.UseSqlServer(connection).UseLoggerFactory(efLogger),//使用dbconnection建立dbcontext支援事務
                         (conStr,builder) => builder.UseSqlServer(conStr).UseLoggerFactory(efLogger));//使用連結字串建立dbcontext
                     op.AddShardingTableRoute<SysUserModVirtualTableRoute>();
                 });
        }
		
		
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
			...
			//新增啟動項
            app.UseShardingCore();
			...
        }
		
		public static class ShardingCoreExtension{

			public static IApplicationBuilder UseShardingCore(this IApplicationBuilder app)
			{
				var shardingBootstrapper = app.ApplicationServices.GetRequiredService<IShardingBootstrapper>();
				shardingBootstrapper.Start();
				return app;
			}
		}

7.控制器使用


        private readonly DefaultShardingDbContext _defaultTableDbContext;

        public ValuesController(DefaultShardingDbContext defaultTableDbContext)
        {
            _defaultTableDbContext = defaultTableDbContext;
        }

        [HttpGet]
        public async Task<IActionResult> Get()
        {
            var resultx11231 = await _defaultTableDbContext.Set<SysUserMod>().Where(o => o.Age == 198198).Select(o=>o.Id).ContainsAsync("1981");
            var resultx1121 = await _defaultTableDbContext.Set<SysUserMod>().Where(o => o.Id == "198").SumAsync(o=>o.Age);
            var resultx111 = await _defaultTableDbContext.Set<SysUserMod>().FirstOrDefaultAsync(o => o.Id == "198");
            var resultx2 = await _defaultTableDbContext.Set<SysUserMod>().CountAsync(o => o.Age<=10);
            var resultx = await _defaultTableDbContext.Set<SysUserMod>().Where(o => o.Id == "198").FirstOrDefaultAsync();
            var resultx33 = await _defaultTableDbContext.Set<SysUserMod>().Where(o => o.Id == "198").Select(o=>o.Id).FirstOrDefaultAsync();
            var resulxxt = await _defaultTableDbContext.Set<SysUserMod>().Where(o => o.Id == "198").ToListAsync();
            var result = await _defaultTableDbContext.Set<SysUserMod>().ToListAsync();

            var sysUserMod98 = result.FirstOrDefault(o => o.Id == "98");
            _defaultTableDbContext.Attach(sysUserMod98);
            sysUserMod98.Name = "name_update"+new Random().Next(1,99)+"_98";
            await _defaultTableDbContext.SaveChangesAsync();
            return Ok(result);
        }

自定義分表鍵,自定義分表規則

目前市面上有的框架要麼對分表欄位有限制比如僅支援DateTime型別或者int等,要麼對分表規則有限制:僅支援按天、按月、取模...等等,但是基於分表規則和分表欄位是業務規則所以本框架遵循將其由業務系統自己定義,最大化來實現分表庫的適用性,基本上滿足一切分表規則,且sharding-core目前預設提供一些常用的分表規則可以快速整合。

預設路由

抽象abstract 路由規則 tail 索引
AbstractSimpleShardingModKeyIntVirtualTableRoute 取模 0,1,2... =
AbstractSimpleShardingModKeyStringVirtualTableRoute 取模 0,1,2... =
AbstractSimpleShardingDayKeyDateTimeVirtualTableRoute 按時間 yyyyMMdd >,>=,<,<=,=,contains
AbstractSimpleShardingDayKeyLongVirtualTableRoute 按時間戳 yyyyMMdd >,>=,<,<=,=,contains
AbstractSimpleShardingWeekKeyDateTimeVirtualTableRoute 按時間 yyyyMMdd_dd >,>=,<,<=,=,contains
AbstractSimpleShardingWeekKeyLongVirtualTableRoute 按時間戳 yyyyMMdd_dd >,>=,<,<=,=,contains
AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute 按時間 yyyyMM >,>=,<,<=,=,contains
AbstractSimpleShardingMonthKeyLongVirtualTableRoute 按時間戳 yyyyMM >,>=,<,<=,=,contains
AbstractSimpleShardingYearKeyDateTimeVirtualTableRoute 按時間 yyyy >,>=,<,<=,=,contains
AbstractSimpleShardingYearKeyLongVirtualTableRoute 按時間戳 yyyy >,>=,<,<=,=,contains

所謂的索引就是通過改對應的條件操作符可以縮小減少指定表的範圍,加快程式的執行
如果以上預設分表無法滿足您的需求您還可以自定義分表,如何分表可以通過繼承 AbstractShardingOperatorVirtualTableRoute<TEntity,TKey>來實現自定義分表規則(近乎90%的規則都可以實現)

動態新增分表資訊

很多分表元件預設不帶動態分表資訊導致很多分表沒辦法根據業務系統來進行動態建立,sharding-core預設提供動態建表介面可以支援動態按時間,按租戶等不需要資料做遷移的動態分表資訊,
如果需要請參考Samples.AutoByDate.SqlServer

支援select,join,group by等連表聚合函式

目前sharding-core支援select按需查詢,join分表連表查詢,group by聚合查詢,雖然本框架支援但是出於效能原因本框架還是不建議使用join操作符來操作,因為過多的表路由會導致笛卡爾積,會導致需要查詢的表集合增長對資料庫連線比較考驗。
以下程式碼來自github的單元測試中,SysUserMod表示使用者表,SysUserSalary表示使用者月薪表使用者表按id取模,使用者月薪表按月分表

//join查詢
var list = await (from u in _virtualDbContext.Set<SysUserMod>()
                              join salary in _virtualDbContext.Set<SysUserSalary>()
                                  on u.Id equals salary.UserId
                              select new
                              {
                                  u.Id,
                                  u.Age,
                                  Salary = salary.Salary,
                                  DateOfMonth = salary.DateOfMonth,
                                  Name = u.Name
                              }).ToListAsync();

//group聚合查詢
var ids = new[] {"200", "300"};
            var dateOfMonths = new[] {202111, 202110};
            var group = await (from u in _virtualDbContext.Set<SysUserSalary>()
                    .Where(o => ids.Contains(o.UserId) && dateOfMonths.Contains(o.DateOfMonth))
                group u by new
                {
                    UId = u.UserId
                }
                into g
                select new
                {
                    GroupUserId = g.Key.UId,
                    Count = g.Count(),
                    TotalSalary = g.Sum(o => o.Salary),
                    AvgSalary = g.Average(o => o.Salary),
                    AvgSalaryDecimal = g.Average(o => o.SalaryDecimal),
                    MinSalary = g.Min(o => o.Salary),
                    MaxSalary = g.Max(o => o.Salary)
                }).ToListAsync();

分頁

我們常說的分頁是分表的難點也是最考驗分表元件的
1我們首先來看普通的分表元件如何分頁
首先我們定義一組組資料比如是1-100的連續數字,然後分成兩張表按奇偶分表

表名 資料
table1 1,3,5,7,9...
table2 2,4,6,8,10...
select * from table limit 2,2理論上結果3,4 
如果本次查詢會做落到table1 和table2那麼會改寫成 2句sql 
第一句 select * from table1 limit 4 ---> 1,3,5,7
第二句 select * from table2 limit 4 ---> 2,4,6,8
將8條資料放入記憶體然後排序
1,2,3,4,5,6,7,8
獲取第3到4條資料 結果[3,4]

這個情況是我們常見的也是最簡單的分頁,但是這個情況僅僅適用於資料量小的時候,如果使用者不小心點到了分頁的最後一頁那麼結果將是災難性的這是毋庸置疑的
那麼sharding-core是如何處理的呢

select * from table limit 2,2
首先還是一樣對資料庫語句進行改性並且生成對應的sql
第一句 select * from table1 limit 4 
第二句 select * from table2 limit 4
因為ado.net預設DataReader是流式獲取,只要連線不關閉那麼可以一直實現next獲取到記憶體
建立一個優先順序佇列一個可以具有排序功能的佇列
因為DataReader的特性我們分別對sql1和sql2進行一次next獲取到2個陣列一個是[1,.....] A和陣列[2......] B
獲取到兩個陣列我們只知道頭部第一個物件因為沒有進行後續的next所以無法知曉剩下的資料但是有一點可以知道後面的資料都是按sql的指定順序的所以都不會比當前頭大或者小
先將1和2放入優先順序佇列可以知道如果asc那麼陣列A放在佇列頭 陣列B放在佇列尾部,然後對優先順序佇列進行poll彈出,並且對A進行next這個時候A變成了[3,....]再將A放入優先順序佇列
這時候優先順序佇列就是B在前A在後依次操作,然後對分頁的進行過濾因為要跳過2個物件所以只需要空執行2次那麼指標就會指向A陣列的3和B陣列的4,剩下的只要獲取2個資料就可以了,
這樣做可以保證記憶體最小化,然後分頁不會成為程式的災難。

無感知使用

目前的分表框架很少有做到無感知使用的,你在使用的時候好一點的框架不依賴三方,一般一點的不但要依賴很多三方框架並且在使用的時候還有一大堆限制,必須使用他的東西還沒辦法做到和dbcontext原生的使用方法。
sharding-core目前使用的是一種類似dbcontext的wrap模式,用一個新的dbcontext來包裝真實的dbcontext,這個包裝的dbcontext我們成為shardingdbcontext,shardingDbContext因為本身也是整合於DbContext所以它的使用方法和原生dbcontext沒有差別。並且僅需少量改動即可支援abp和abp.next

讀寫分離的支援

目前sharding-core已經支援單node節點的讀寫分離操作,將在不久的未來(1-2)天內支援多節點的讀寫分離


            services.AddShardingDbContext<ShardingDefaultDbContext, DefaultDbContext>(o => o.UseSqlServer(hostBuilderContext.Configuration.GetSection("SqlServer")["ConnectionString"])
                ,op =>
                {
                    op.EnsureCreatedWithOutShardingTable = true;
                    op.CreateShardingTableOnStart = true;
                    op.UseShardingOptionsBuilder((connection, builder) => builder.UseSqlServer(connection).UseLoggerFactory(efLogger),
                        (conStr,builder)=> builder.UseSqlServer("read db connection string").UseLoggerFactory(efLogger));
                    op.AddShardingTableRoute<SysUserModVirtualTableRoute>();
                    op.AddShardingTableRoute<SysUserSalaryVirtualTableRoute>();
                });

未來計劃將支援分庫,支援強制路由,顯示路由等...
最後具體如何使用且使用方式可以參考github(https://github.com/xuejmnet/sharding-core) 當然我也會在後續出一系列的部落格來對框架進行支援的介紹

最後的最後

該文件是我晚上趕工趕出來的也想趁熱打鐵希望更多的人關注,也希望更多的人可以交流。

憑藉各大開源生態圈提供的優秀程式碼和思路才有的這個框架,希望可以為.Net生態提供一份微薄之力,該框架本人會一直長期維護,有大神技術支援可以聯絡下方方式歡迎star ?

部落格

QQ群:771630778

個人QQ:326308290(歡迎技術支援提供您寶貴的意見)

個人郵箱:326308290@qq.com

相關文章