efcore分表分庫原理解析

薛家明發表於2021-10-09

ShardingCore

ShardingCore 易用、簡單、高效能、普適性,是一款擴充套件針對efcore生態下的分表分庫的擴充套件解決方案,支援efcore2+的所有版本,支援efcore2+的所有資料庫、支援自定義路由、動態路由、高效能分頁、讀寫分離的一款元件,如果你喜歡這元件或者這個元件對你有幫助請點選下發star讓更多的.neter可以看到使用


Gitee Star 助力dotnet 生態 Github Star


經過了3個星期再次發一篇部落格來介紹本框架的實現原理通過本篇文章可以有助於您閱讀原始碼和提出寶貴意見。之前通過兩篇文章簡單的介紹了sharding-core的核心聚合原理(ShardingCore 如何呈現“完美”分表)和高效能分頁原理實現(ShardingCore是如何針對分表下的分頁進行優化的),這兩篇文章主要是針對分表分庫下資料獲取的一個解決方案的思路並不涉及到太多efcore(.net)的知識。

通過關係圖我們可以看到目前一個shardingdbcontext下主要是以entity作為媒介通過兩個虛擬表和虛擬資料來源為橋樑來實現一對多的關係對映

首先先說下經過了3個星期目前本框架已經具有了3個星期前不具備的一些功能,主要是有以下幾個功能上的改進和新增

分庫支援

之前的框架僅支援分表,思路是先將分表做到相對完成度比較高後在實現分庫,畢竟分表對於大部分使用者而言使用場景更高,目前已經實現針對資料物件實現了分庫的實現,當然您還是可以在分庫的基礎上在實現分表,這兩者是不衝突的

services.AddShardingDbContext<DefaultShardingDbContext, DefaultDbContext>(
                    o =>
                        o.UseSqlServer("Data Source=localhost;Initial Catalog=ShardingCoreDBxx0;Integrated Security=True;")
                ).Begin(o =>
                {
                    o.CreateShardingTableOnStart = true;
                    o.EnsureCreatedWithOutShardingTable = true;
                })
                .AddShardingQuery((conStr, builder) => builder.UseSqlServer(conStr).UseLoggerFactory(efLogger)
                    .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking))
                .AddShardingTransaction((connection, builder) =>
                    builder.UseSqlServer(connection).UseLoggerFactory(efLogger))
                .AddDefaultDataSource("ds0","Data Source=localhost;Initial Catalog=ShardingCoreDBxx0;Integrated Security=True;")
                .AddShardingDataSource(sp =>//新增額外兩個資料來源一共3個庫
                {
                    return new Dictionary<string, string>()
                    {
                        {"ds1", "Data Source=localhost;Initial Catalog=ShardingCoreDBxx1;Integrated Security=True;"},
                        {"ds2", "Data Source=localhost;Initial Catalog=ShardingCoreDBxx2;Integrated Security=True;"},
                    };
                }).AddShardingDataSourceRoute(o =>
                {
                    o.AddShardingDatabaseRoute<SysUserModVirtualDataSourceRoute>();
                }).End();

支援code-first

相信很多使用efcore的使用者其實是更加喜歡脫離資料庫開發,在開發的時候不進行資料庫層面的操作而只專注於程式碼的業務編寫來保證高效性,配合efcore的fluent api 可以做到很完美的開發時候不關注資料庫,效率拉滿 Migrations



//建立遷移sqlgenerator
    /// <summary>
    /// https://github.com/Coldairarrow/EFCore.Sharding/blob/master/src/EFCore.Sharding.SqlServer/ShardingSqlServerMigrationsSqlGenerator.cs
    /// </summary>
    public class ShardingSqlServerMigrationsSqlGenerator<TShardingDbContext> : SqlServerMigrationsSqlGenerator where TShardingDbContext:DbContext,IShardingDbContext
    {
        public ShardingSqlServerMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies, IRelationalAnnotationProvider migrationsAnnotations) : base(dependencies, migrationsAnnotations)
        {
        }
        protected override void Generate(
            MigrationOperation operation,
            IModel model,
            MigrationCommandListBuilder builder)
        {
            var oldCmds = builder.GetCommandList().ToList();
            base.Generate(operation, model, builder);
            var newCmds = builder.GetCommandList().ToList();
            var addCmds = newCmds.Where(x => !oldCmds.Contains(x)).ToList();

            MigrationHelper.Generate<TShardingDbContext>(operation, builder, Dependencies.SqlGenerationHelper, addCmds);
        }
    }
//新增遷移codefirst的contextfactory基本和starup一樣如果是以非命令執行比如 `_context.Database.Migrate()`那麼startup也需要新增` .ReplaceService<IMigrationsSqlGenerator, ShardingSqlServerMigrationsSqlGenerator<DefaultShardingTableDbContext>>()`

    public class DefaultDesignTimeDbContextFactory: IDesignTimeDbContextFactory<DefaultShardingTableDbContext>
    { 
        static DefaultDesignTimeDbContextFactory()
        {
            var services = new ServiceCollection();
            services.AddShardingDbContext<DefaultShardingTableDbContext, DefaultTableDbContext>(
                    o =>
                        o.UseSqlServer("Data Source=localhost;Initial Catalog=ShardingCoreDBMigration;Integrated Security=True;")
                            .ReplaceService<IMigrationsSqlGenerator, ShardingSqlServerMigrationsSqlGenerator<DefaultShardingTableDbContext>>()//區別替換掉原先的遷移
                ).Begin(o =>
                {
                    o.CreateShardingTableOnStart = false;
                    o.EnsureCreatedWithOutShardingTable = false;
                    o.AutoTrackEntity = true;
                })
                .AddShardingQuery((conStr, builder) => builder.UseSqlServer(conStr)
                    .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking))
                .AddShardingTransaction((connection, builder) =>
                    builder.UseSqlServer(connection))
                .AddDefaultDataSource("ds0",
                    "Data Source=localhost;Initial Catalog=ShardingCoreDBMigration;Integrated Security=True;")
                .AddShardingTableRoute(o =>
                {
                    o.AddShardingTableRoute<ShardingWithModVirtualTableRoute>();
                    o.AddShardingTableRoute<ShardingWithDateTimeVirtualTableRoute>();
                }).End();
            services.AddLogging();
            var buildServiceProvider = services.BuildServiceProvider();
            ShardingContainer.SetServices(buildServiceProvider);
            new ShardingBootstrapper(buildServiceProvider).Start();
        }

        public DefaultShardingTableDbContext CreateDbContext(string[] args)
        {
            return ShardingContainer.GetService<DefaultShardingTableDbContext>();
        }
}


1.初始化新增遷移(Add-Migration EFCoreSharding -Context DefaultShardingTableDbContext -OutputDir Migrations\ShardingMigrations)
2.更新資料庫(Update-Database -Context DefaultShardingTableDbContext -Verbose)
3.獲取遷移指令碼( Script-Migration -Context DefaultShardingTableDbContext)用於生產環境

支援自動追蹤

efcore的好用功能之一(自動追蹤)開啟後可以幫助程式實現更多的功能,雖然之前也是支援的但是就是用體驗而言之前的需要手動attach而目前支援了自動化,當然也不可能和efcore原生的100%完美,當然框架預設不開啟自動追蹤

services.AddShardingDbContext<DefaultShardingTableDbContext, DefaultTableDbContext>(
                    o =>
                        o.UseSqlServer("Data Source=localhost;Initial Catalog=ShardingCoreDBMigration;Integrated Security=True;")
                            .ReplaceService<IMigrationsSqlGenerator,ShardingSqlServerMigrationsSqlGenerator<DefaultShardingTableDbContext>>()
                ).Begin(o =>
                {
                    o.CreateShardingTableOnStart = false;
                    o.EnsureCreatedWithOutShardingTable = false;
                    o.AutoTrackEntity = true;//新增對應程式碼可以讓整個框架進行自動追蹤支援
                })
                .AddShardingQuery((conStr, builder) => builder.UseSqlServer(conStr))
                .AddShardingTransaction((connection, builder) =>
                    builder.UseSqlServer(connection))
                .AddDefaultDataSource("ds0",
                    "Data Source=localhost;Initial Catalog=ShardingCoreDBMigration;Integrated Security=True;")
                .AddShardingTableRoute(o =>
                {
                    o.AddShardingTableRoute<ShardingWithModVirtualTableRoute>();
                    o.AddShardingTableRoute<ShardingWithDateTimeVirtualTableRoute>();
                }).End();

單次查詢核心執行緒數控制

說人話就是本次查詢路由坐落到10張表,之前的做法是開啟10個執行緒並行查詢10次後獲取到對應的迭代器,目前新增了核心查詢執行緒數控制,如果您設定了5,本次查詢路由到10張表,會議開始開啟5個執行緒,後續每完成一個開啟一個新新執行緒,並且支援超時時間,可以保證在一定時間內執行完成,完不成就超時,防止查詢坐落的表過多而一次性大量開啟執行緒從而導致程式消耗過多資源

.Begin(o =>
                {
                    o.CreateShardingTableOnStart = true;
                    o.EnsureCreatedWithOutShardingTable = true;
                    o.AutoTrackEntity = true;
                    o.ParallelQueryMaxThreadCount = 10;//併發查詢執行緒數
                    o.ParallelQueryTimeOut=TimeSpan.FromSeconds(10);//查詢併發等待超時時間
                }

讀寫分離延遲處理

框架目前支援全域性定義和區域性定義是否啟用讀寫分離,如果您開啟了讀寫分離那麼資料庫和資料庫之間的資料同步延遲會是一個很嚴重的問題他會讓你沒辦法很好的查詢到剛修改的資料,而sharding-core為這個場景提供了手動切換是否使用writeonly字串;用來保證消除讀寫分離時帶來的延遲,而造成資料處理上的異常。而且程式也提供了讀寫分離策略除了隨機和輪詢外額外有一個配置可以配置讀寫分離真正執行是依據dbcontext還是每次都是最新的,每次都是最新的會有一個問題,你明明分頁count出來是10條可能查詢只返回了9條或者其他資料,所以再次基礎上進行了設定是否按dbcontext就是說同一個dbcontext是一樣的連結,dbcontext預設是scope就是說一次請求下面是一樣的當然也可以設定成每次都是最新的具體自行考慮根據業務

以上一些功能的新增和優化是之前sharding-core版本所不具備的,其他功能也在不斷的完善中。
接下來我將來講解下sharding-core的實現原理如何讓efcore實現sharding功能,並且完美的無感知使用dbcontext。

ShardingDbContext的擴充套件

在sharding-core中核心api介面依然是通過dbcontext的繼承來實現的,首先是攔截sql,總的有兩條路可以走1.通過efcore提供的攔截器攔截sql配合antlr4實現對sql語句的分析和從新分裂出對應的語句來進行查詢最後通過多個datareader進行流式聚合。2.通過攔截iqueryable的lambda表示式來分裂成多個ienumerator進行聚合,在這裡我選擇了後者因為相比表示式的解析字串的解析更加吃力而且本人也不是很熟悉antlr4所以選擇了後者。那麼如何進行攔截的,這個熟悉linq的同學肯定都知道一個iqueryable都會有一個對應的provider這兩個是一對的,又得益於efcore的開放型設計通過替換兩個核心介面來實現IDbSetSource IQueryCompiler,下面就簡單說下這兩個介面在efcore中的作用

IDbSetSource

用於針對efcore的dbcontext.set<entity>()dbset<entity>()進行攔截和api重構具體是現代嗎ShardingDbSetSource

IQueryCompiler

efcore核心查詢編譯,用於對錶達式進行編譯後快取起來,所有的查詢都會通過IQueryCompiler核心介面,那麼通過自己實現這兩個介面接管對應的表示式後對錶達式進行分析就可以獲取到對應的where子句,在通過將表示式進行路由後並行請求流式聚合返回對應的IEnumerator或者IAsyncEnumerator就可以實現無感知使用sharding-core,感覺和使用efcore一毛一樣。具體實現程式碼ShardingQueryCompiler

AtcualDbContext擴充套件

用過efcore的都應該知道目前efcore的機制就是一個物件一張表,在這個機制下面如果你想實現上圖的功能只能建立多個dbcontext然後讓對應的dbcontext的物件對映到對應的表裡面而不是固定的Entitiy對應table,那麼如何讓對應的物件Entity對應table1和table2和table3呢?

//dbcontext下的這個方法在dbcontext被建立後第一次呼叫Model屬性會被載入如果快取已存在那麼不會被多次載入
protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
        }

說人話就是我可以再這邊通過modelBuilder獲取我自己想要的物件但是如果我把Entity對映到了table1那麼這個dbcontext就會被快取起來entity-table1這個關係也會被快取起來沒辦法改變了,那麼是否有辦法可以解決這個機制呢有兩個efcore的介面可以幫助我們實現這個功能,這個在部落格園很多大神都已經實現過了具體是 IModelCacheKeyFactoryIModelCustomizer

IModelCacheKeyFactory

用於將efcore的模型快取進行判斷是否和之前的模型快取一致具體實現ShardingModelCacheKeyFactory


    public class ShardingModelCacheKeyFactory : IModelCacheKeyFactory
    {
        public object Create(DbContext context)
        {
            if (context is IShardingTableDbContext shardingTableDbContext&&!string.IsNullOrWhiteSpace(shardingTableDbContext.RouteTail.GetRouteTailIdentity()))
            {
                
                return $"{context.GetType()}_{shardingTableDbContext.RouteTail.GetRouteTailIdentity()}";
            }
            else
            {
                return context.GetType();
            }
        }
    }

IModelCustomizer

這個介面是efcore開放出來在模型快取結構定義完成後初始化快取前可以使用的介面,就是說我們並不需要在OnModelCreating方法中使用或者說不需要再次地方進行修改可以在IModelCustomizer介面內部實現,具體程式碼ShardingModelCustomizer


    public class ShardingModelCustomizer<TShardingDbContext> : ModelCustomizer where TShardingDbContext : DbContext, IShardingDbContext
    {
        private Type _shardingDbContextType => typeof(TShardingDbContext);

        public ShardingModelCustomizer(ModelCustomizerDependencies dependencies) : base(dependencies)
        {
        }

        public override void Customize(ModelBuilder modelBuilder, DbContext context)
        {
            base.Customize(modelBuilder, context);
            if (context is IShardingTableDbContext shardingTableDbContext&& shardingTableDbContext.RouteTail.IsShardingTableQuery())
            {
                var isMultiEntityQuery = shardingTableDbContext.RouteTail.IsMultiEntityQuery();
                if (!isMultiEntityQuery)
                {
                    var singleQueryRouteTail = (ISingleQueryRouteTail) shardingTableDbContext.RouteTail;
                    var tail = singleQueryRouteTail.GetTail();
                    var virtualTableManager = ShardingContainer.GetService<IVirtualTableManager<TShardingDbContext>>();
                    var typeMap = virtualTableManager.GetAllVirtualTables().Where(o => o.GetTableAllTails().Contains(tail)).Select(o => o.EntityType).ToHashSet();

                    //設定分表
                    var mutableEntityTypes = modelBuilder.Model.GetEntityTypes().Where(o => o.ClrType.IsShardingTable() && typeMap.Contains(o.ClrType));
                    foreach (var entityType in mutableEntityTypes)
                    {
                        MappingToTable(entityType.ClrType, modelBuilder, tail);
                    }
                }
                else
                {
                    var multiQueryRouteTail = (IMultiQueryRouteTail) shardingTableDbContext.RouteTail;
                    var entityTypes = multiQueryRouteTail.GetEntityTypes();
                    var mutableEntityTypes = modelBuilder.Model.GetEntityTypes().Where(o => o.ClrType.IsShardingTable() && entityTypes.Contains(o.ClrType)).ToArray();
                    foreach (var entityType in mutableEntityTypes)
                    {
                        var queryTail = multiQueryRouteTail.GetEntityTail(entityType.ClrType);
                        if (queryTail != null)
                        {
                            MappingToTable(entityType.ClrType, modelBuilder, queryTail);
                        }
                    }
                }
            }
        }

        private void MappingToTable(Type clrType, ModelBuilder modelBuilder, string tail)
        {
            var shardingEntityConfig = ShardingUtil.Parse(clrType);
            var shardingEntity = shardingEntityConfig.EntityType;
            var tailPrefix = shardingEntityConfig.TailPrefix;
            var entity = modelBuilder.Entity(shardingEntity);
            var tableName = shardingEntityConfig.VirtualTableName;
            if (string.IsNullOrWhiteSpace(tableName))
                throw new ArgumentNullException($"{shardingEntity}: not found original table name。");
#if DEBUG
            Console.WriteLine($"mapping table :[tableName]-->[{tableName}{tailPrefix}{tail}]");
#endif
            entity.ToTable($"{tableName}{tailPrefix}{tail}");
        }
    }

稍作解析進入後會先判斷dbcontext真正執行的那個是否是需要分表的並且判斷本次查詢涉及到的表示一張還是多張,對此物件在資料庫裡的對映關係改成分表

到此為止efcore的查詢架構已經算是非常清晰了

  • 通過替換模型快取介面和查詢編譯介面來實現查詢編譯時攔截sql和模型重建
  • 通過類似介面卡模式來實現對外dbcontext其實內部有多個dbcontext在進行真正的工作

上述幾步讓sharding-core在使用上和efcore一樣除了配置方面,後續將會出更多的efcore的分表分庫實踐文章和繼續開發完成其他orm的支援,當然這個改動將會非常大也希望各位.neter有喜歡的或者希望瞭解原始碼的或者想參與完善的多多支援

下一篇實現如何自定義路由,自定義路由的原理 where left

最後

本人會一致維護該框架,希望為.net生態做一份共享

Gitee Star 助力dotnet 生態 Github Star


部落格

QQ群:771630778

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

個人郵箱:326308290@qq.com

相關文章