diff --git a/README-zh.md b/README-zh.md index 26a6ad02..49630c3f 100644 --- a/README-zh.md +++ b/README-zh.md @@ -1,7 +1,7 @@

ShardingCore

-`ShardingCore` 易用、简单、高性能、普适性,是一款扩展针对efcore生态下的分表分库的扩展解决方案,支持efcore2+的所有版本,支持efcore2+的所有数据库、支持自定义路由、动态路由、高性能分页、读写分离的一款组件,如果你喜欢这组件或者这个组件对你有帮助请点击下发star让更多的.neter可以看到使用 +`ShardingCore` 一款`ef-core`下高性能、轻量级针对分表分库读写分离的解决方案,具有零依赖、零学习成本、零业务代码入侵。 --- @@ -17,25 +17,288 @@ [中文文档Gitee](https://xuejm.gitee.io/sharding-core-doc/) | [English Document Gitee](https://xuejm.gitee.io/sharding-core-doc/en/) -### 依赖 +## 依赖 Release | EF Core | .NET | .NET (Core) --- | --- | --- | --- -[6.x.x.x](https://www.nuget.org/packages/ShardingCore) | >= 6.0.0 | net 6.0 | 6.0+ -[5.x.x.x](https://www.nuget.org/packages/ShardingCore) | >= 5.0.10 | Standard 2.1 | 5.0+ +[6.x.x.x](https://www.nuget.org/packages/ShardingCore) | 6.0.0 | net 6.0 | 6.0+ +[5.x.x.x](https://www.nuget.org/packages/ShardingCore) | 5.0.10 | Standard 2.1 | 5.0+ [3.x.x.x](https://www.nuget.org/packages/ShardingCore) | 3.1.18 | Standard 2.0 | 2.0+ [2.x.x.x](https://www.nuget.org/packages/ShardingCore) | 2.2.6 | Standard 2.0 | 2.0+ -### 数据库支持 -数据库 | 是否支持 | 支持情况 ---- | --- | --- -SqlServer | 支持 | 支持 -MySql |支持 | 支持 -PostgreSql | 支持 | 支持 -SQLite | 支持 | 未测试 -Oracle | 支持 | 未测试 -其他 | 支持(只要efcore支持) | 未测试 +## 快速开始 +5步实现按月分表,且支持自动化建表建库 +### 第一步安装依赖 +`ShardingCore`版本表现形式为a.b.c.d,其中a表示`efcore`的版本号,b表示`ShardingCore`的主版本号,c表示`ShardingCore`次级版本号,d表示`ShardingCore`的修订版本号 +```shell +# 请对应安装您需要的版本 +PM> Install-Package ShardingCore +# use sqlserver +PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer +# use mysql +#PM> Install-Package Pomelo.EntityFrameworkCore.MySql +# use other database driver,if efcore support +``` + +### 第二步创建查询对象 + +查询对象 +```csharp + + /// + /// order table + /// + public class Order + { + /// + /// order Id + /// + public string Id { get; set; } + /// + /// payer id + /// + public string Payer { get; set; } + /// + /// pay money cent + /// + public long Money { get; set; } + /// + /// area + /// + public string Area { get; set; } + /// + /// order status + /// + public OrderStatusEnum OrderStatus { get; set; } + /// + /// CreationTime + /// + public DateTime CreationTime { get; set; } + } + public enum OrderStatusEnum + { + NoPay=1, + Paying=2, + Payed=3, + PayFail=4 + } +``` +### 第三步创建dbcontext +dbcontext `AbstractShardingDbContext`和`IShardingTableDbContext`如果你是普通的DbContext那么就继承`AbstractShardingDbContext`需要分表就实现`IShardingTableDbContext`,如果只有分库可以不实现`IShardingTableDbContext`接口 +```csharp + + public class MyDbContext:AbstractShardingDbContext,IShardingTableDbContext + { + public MyDbContext(DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(entity => + { + entity.HasKey(o => o.Id); + entity.Property(o => o.Id).IsRequired().IsUnicode(false).HasMaxLength(50); + entity.Property(o=>o.Payer).IsRequired().IsUnicode(false).HasMaxLength(50); + entity.Property(o => o.Area).IsRequired().IsUnicode(false).HasMaxLength(50); + entity.Property(o => o.OrderStatus).HasConversion(); + entity.ToTable(nameof(Order)); + }); + } + /// + /// empty impl if use sharding table + /// + public IRouteTail RouteTail { get; set; } + } +``` + +### 第四步添加分表路由 + +```csharp + + public class OrderVirtualTableRoute:AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute + { + /// + /// fix value don't use DateTime.Now because if if application restart this value where change + /// + /// + public override DateTime GetBeginTime() + { + return new DateTime(2021, 1, 1); + } + /// + /// configure sharding property + /// + /// + + public override void Configure(EntityMetadataTableBuilder builder) + { + builder.ShardingProperty(o => o.CreationTime); + } + /// + /// enable auto create table job + /// + /// + + public override bool AutoCreateTableByTime() + { + return true; + } + } +``` + +### 第五步配置启动项 +无论你是何种数据库只需要修改`AddDefaultDataSource`里面的链接字符串 请不要修改委托内部的UseXXX参数 `conStr` and `connection` +```csharp + + public void ConfigureServices(IServiceCollection services) + { + + services.AddShardingDbContext((conStr, builder) => + { + //don't modify conStr, params should use delegate input params + builder.UseSqlServer(conStr); + }).Begin(op => + { + op.AutoTrackEntity = true; + //if use code-first should false + op.CreateShardingTableOnStart = true; + //if use code-first should false + op.EnsureCreatedWithOutShardingTable = true; + }).AddShardingTransaction((connection, builder) => + { + //don't modify connection, params should use delegate input params + builder.UseSqlServer(connection); + }).AddDefaultDataSource("ds0", + "Data Source=localhost;Initial Catalog=EFCoreShardingTableDB;Integrated Security=True;") + .AddShardingTableRoute(op => + { + op.AddShardingTableRoute(); + }).End(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + // it's importment don't forget it + app.ApplicationServices.GetRequiredService().Start(); + // other configure.... + } +``` +这样所有的配置就完成了你可以愉快地对Order表进行按月分表了 + +```csharp +[Route("api/[controller]")] +public class ValuesController : Controller +{ + private readonly MyDbContext _myDbContext; + + public ValuesController(MyDbContext myDbContext) + { + _myDbContext = myDbContext; + } + + [HttpGet] + public async Task Get() + { + var order = await _myDbContext.Set().FirstOrDefaultAsync(o => o.Id == "2"); + return OK(order) + } +} +``` + + +## 性能 + + +以下所有数据均在开启了表达式编译缓存的情况下测试,并且电脑处于长时间未关机并且开着很多vs和idea的情况下仅供参考,所有测试都是基于ShardingCore x.3.1.63+ version + +以下所有数据均在[源码中有案例](https://github.com/xuejmnet/sharding-core/blob/main/benchmarks/ShardingCoreBenchmark/EFCoreCrud.cs) + +efcore版本均为6.0 表结构为string型id的订单取模分成5张表 + +N代表执行次数 + +### 性能损耗 sql server 2012,data rows 7734363 =773w + +// * Summary * + +BenchmarkDotNet=v0.13.1, OS=Windows 10.0.18363.1500 (1909/November2019Update/19H2) +AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores +.NET SDK=6.0.100 + [Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT + DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT + + +| Method | N | Mean | Error | StdDev | +|----------------------------------- |--- |---------:|----------:|----------:| +| NoShardingIndexFirstOrDefaultAsync | 10 | 1.512 ms | 0.0071 ms | 0.0063 ms | +| ShardingIndexFirstOrDefaultAsync | 10 | 1.567 ms | 0.0127 ms | 0.0113 ms | + +针对未分片数据的查询性能,可以看出10次查询差距为0.05ms,单次查询损耗约为5微妙=0.005毫秒,损耗占比为3%, + +结论:efcore 原生查询和sharding-core的查询在针对未分片对象查询上性能可达原先的97%具有极高的性能 + +### 性能测试 + + +#### sql server 2012,data rows 7734363 =773w + +// * Summary * + +BenchmarkDotNet=v0.13.1, OS=Windows 10.0.18363.1500 (1909/November2019Update/19H2) +AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores +.NET SDK=6.0.100 + [Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT + DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT + + +| Method | N | Mean | Error | StdDev | Median | +|------------------------------------- |--- |-------------:|------------:|------------:|-------------:| +| NoShardingIndexFirstOrDefaultAsync | 10 | 2.154 ms | 0.1532 ms | 0.4443 ms | 1.978 ms | +| ShardingIndexFirstOrDefaultAsync | 10 | 4.293 ms | 0.1521 ms | 0.4485 ms | 4.077 ms | +| NoShardingNoIndexFirstOrDefaultAsync | 10 | 823.382 ms | 16.0849 ms | 18.5233 ms | 821.221 ms | +| ShardingNoIndexFirstOrDefaultAsync | 10 | 892.276 ms | 17.8131 ms | 16.6623 ms | 894.880 ms | +| NoShardingNoIndexCountAsync | 10 | 830.754 ms | 16.5309 ms | 38.6405 ms | 821.736 ms | +| ShardingNoIndexCountAsync | 10 | 915.630 ms | 8.8511 ms | 7.3911 ms | 914.107 ms | +| NoShardingNoIndexLikeToListAsync | 10 | 7,008.918 ms | 139.4664 ms | 166.0248 ms | 6,955.674 ms | +| ShardingNoIndexLikeToListAsync | 10 | 7,044.168 ms | 135.3814 ms | 132.9626 ms | 7,008.057 ms | +| NoShardingNoIndexToListAsync | 10 | 787.129 ms | 10.5812 ms | 8.8357 ms | 785.798 ms | +| ShardingNoIndexToListAsync | 10 | 935.880 ms | 16.3354 ms | 15.2801 ms | 940.369 ms | + +#### mysql 5.7,data rows 7553790=755w innerdb_buffer_size=3G + + +// * Summary * + +BenchmarkDotNet=v0.13.1, OS=Windows 10.0.18363.1500 (1909/November2019Update/19H2) +AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores +.NET SDK=6.0.100 + [Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT + DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT + + +| Method | N | Mean | Error | StdDev | Median | +|------------------------------------- |--- |--------------:|------------:|--------------:|--------------:| +| NoShardingIndexFirstOrDefaultAsync | 10 | 5.020 ms | 0.1245 ms | 0.3672 ms | 4.855 ms | +| ShardingIndexFirstOrDefaultAsync | 10 | 7.960 ms | 0.1585 ms | 0.2514 ms | 7.974 ms | +| NoShardingNoIndexFirstOrDefaultAsync | 10 | 11,336.083 ms | 623.8044 ms | 1,829.5103 ms | 11,185.590 ms | +| ShardingNoIndexFirstOrDefaultAsync | 10 | 5,422.259 ms | 77.5386 ms | 72.5296 ms | 5,390.019 ms | +| NoShardingNoIndexCountAsync | 10 | 14,229.819 ms | 82.8929 ms | 77.5381 ms | 14,219.773 ms | +| ShardingNoIndexCountAsync | 10 | 3,085.268 ms | 55.5942 ms | 49.2828 ms | 3,087.704 ms | +| NoShardingNoIndexLikeToListAsync | 10 | 27,046.390 ms | 71.2034 ms | 59.4580 ms | 27,052.316 ms | +| ShardingNoIndexLikeToListAsync | 10 | 5,707.009 ms | 106.8713 ms | 99.9675 ms | 5,672.453 ms | +| NoShardingNoIndexToListAsync | 10 | 26,001.850 ms | 89.2787 ms | 69.7030 ms | 25,998.407 ms | +| ShardingNoIndexToListAsync | 10 | 5,490.659 ms | 71.8199 ms | 67.1804 ms | 5,477.891 ms | + +具体可以通过first前两次结果来计算得出结论单次查询的的损耗为0.2-0.3毫秒之间,通过数据聚合和数据路由的损耗单次在0.2ms-0.3ms,其中创建dbcontext为0.1毫秒目前没有好的优化方案,0.013毫秒左右是路由表达式解析和编译,复杂表达式可能更加耗时,剩下的0.2毫秒为数据源和表后缀的解析等操作包括实例的反射创建和数据的聚合, +sqlserver的各项数据在分表和未分表的情况下都几乎差不多可以得出在770w数据集情况下数据库还并未是数据瓶颈的关键,但是mysql可以看到在分表和未分表的情况下如果涉及到没有索引的全表扫描那么性能的差距将是分表后的表数目之多,测试中为5-6倍,也就是分表数目 + - [使用介绍](#使用介绍) - [简介](#简介) @@ -57,6 +320,7 @@ Oracle | 支持 | 未测试 - [批量操作](#批量操作) - [读写分离](#读写分离) - [高性能分页](#高性能分页) + - [表达式缓存](#表达式缓存) - [注意事项](#注意事项) - [计划(Future)](#计划) - [最后](#最后) @@ -627,7 +891,12 @@ var list = new List(); ## 读写分离 该框架目前已经支持一主多从的读写分离`AddReadWriteSeparation`,支持轮询 Loop和随机 Random两种读写分离策略,又因为读写分离多链接的时候会导致数据读写不一致,(如分页其实是2步第一步获取count,第二部获取list)会导致数据量在最后几页出现缺量的问题, 针对这个问题框架目前实现了自定义读链接获取策略`ReadConnStringGetStrategyEnum.LatestEveryTime`表示为每次都是新的(这个情况下会出现上述问题),`ReadConnStringGetStrategyEnum.LatestFirstTime`表示以dbcontext作为单位获取一次(同dbcontext不会出现问题), -又因为各节点读写分离网络等一系列问题会导致刚刚写入的数据没办法获取到所以系统默认在dbcontext上添加是否使用读写分离如果false默认选择写字符串去读取`_defaultTableDbContext.ReadWriteSeparation=false` +又因为各节点读写分离网络等一系列问题会导致刚刚写入的数据没办法获取到所以系统默认在dbcontext上添加是否使用读写分离如果false默认选择写字符串去读取`_defaultTableDbContext.ReadWriteSeparation=false`或者使用两个封装好的方法 +```c# + //切换到只读数据库,只读数据库又只配置了A数据源读取B数据源 + _virtualDbContext.ReadWriteSeparationReadOnly(); + _virtualDbContext.ReadWriteSeparationWriteOnly(); +``` ```c# services.AddShardingDbContext( @@ -654,11 +923,12 @@ services.AddShardingDbContext( "Data Source=localhost;Initial Catalog=ShardingCoreDBReadOnly2;Integrated Security=True;"} } }; - }, ReadStrategyEnum.Loop).End(); + }, ReadStrategyEnum.Loop,defaultEnable:true).End(); + _virtualDbContext.ReadWriteSeparationReadOnly(); //reslove read write delay data not found - _defaultTableDbContext.ReadWriteSeparation=false; //dbcontext use write connection string + _virtualDbContext.ReadWriteSeparationWriteOnly(); ``` ## 高性能分页 @@ -708,6 +978,50 @@ var shardingPageResultAsync = await _defaultTableDbContext.Set().Ord ``` ### 注意:如果你是按时间排序无论何种排序建议开启并且加上时间顺序排序,如果你是取模或者自定义分表,建议将Id作为顺序排序,如果没有特殊情况请使用id排序并且加上反向排序作为性能优化,如果entity同时支持分表分库并且两个路由都支持同一个属性的顺序排序优先级为先分库后分表 +## 表达式缓存 +可以通过路由开启表达式缓存针对单个tail的表达式进行缓存支持=,>,>=,<,<=,equal +```c# + + public class OrderCreateTimeVirtualTableRoute:AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute + { + //开启表达式缓存 + public override bool EnableRouteParseCompileCache => true; + } +``` +针对表达式缓存可以自行重写父类方法来自行实现,也可以仅实现多tail表达式`AbstractShardingRouteParseCompileCacheVirtualTableRoute`,`AbstractShardingRouteParseCompileCacheVirtualDataSourceRoute` +```c# + public virtual Func CachingCompile(Expression> parseWhere) + { + if (EnableRouteParseCompileCache) + { + var doCachingCompile = DoCachingCompile(parseWhere); + if (doCachingCompile != null) + return doCachingCompile; + doCachingCompile = CustomerCachingCompile(parseWhere); + if (doCachingCompile != null) + return doCachingCompile; + } + return parseWhere.Compile(); + } + /// + /// 系统默认永久单表达式缓存 + /// + /// + /// 返回null会走这个方法如果还是null就会调用方法 + protected virtual Func DoCachingCompile(Expression> parseWhere) + { + var shouldCache = ShouldCache(parseWhere); + if(shouldCache) + return _routeCompileCaches.GetOrAdd(parseWhere, key => parseWhere.Compile()); + return null; + } + protected virtual Func CustomerCachingCompile(Expression> parseWhere) + { + return null; + } +``` + +开启表达式缓存可以将路由模块的编译由原先的0.14ms升级到0.013ms提示约0.13ms将近10倍性能 # 注意事项 使用该框架需要注意两点如果你的shardingdbcontext重写了以下服务可能无法使用 如果还想使用需要自己重写扩展[请参考](https://github.com/xuejmnet/sharding-core/blob/main/src/ShardingCore/DIExtension.cs)