您现在的位置是:网站首页> 编程资料编程资料

.Net使用分表分库框架ShardingCore实现多字段分片_实用技巧_

2023-05-24 404人已围观

简介 .Net使用分表分库框架ShardingCore实现多字段分片_实用技巧_

介绍

本期主角:ShardingCore 一款ef-core下高性能、轻量级针对分表分库读写分离的解决方案,具有零依赖、零学习成本、零业务代码入侵

dotnet下唯一一款全自动分表,多字段分表框架,拥有高性能,零依赖、零学习成本、零业务代码入侵,并且支持读写分离动态分表分库,同一种路由可以完全自定义的新星组件,通过本框架你不但可以学到很多分片的思想和技巧,并且更能学到Expression的奇思妙用

你的star和点赞是我坚持下去的最大动力,一起为.net生态提供更好的解决方案

项目地址

github地址https://github.com/xuejmnet/sharding-core

gitee地址https://gitee.com/dotnetchina/sharding-core

背景

直接开门见山,你有没有这种情况你需要将一批数据用时间分片来进行存储比如订单表,订单表的分片字段是订单的创建时间,并且id是雪花id订单编号是带时间信息的编号,因为.net下的所有分片方案几乎都是只支持单分片字段,所以当我们不使用分片字段查询也就是订单创建时间查询的话会带来全表查询,导致性能下降,譬如我想用雪花id或者订单编号进行查询,但是带来的却是内部低效的结果,针对这种情况是否有一个好的解决方案呢,有但是需要侵入业务代码,根据雪花id或者订单编号进行解析出对应的时间然后手动指定分片前提是框架支持手动指定.基于上述原因ShardingCore 带来了全新版本 x.3.2.x+ 支持多字段分片路由,并且拥有很完美的实现,废话不多说我们直接开始吧!!!!!!!!!!!

原理

我们现在假定一个很简单的场景,依然是订单时间按月分片,查询进行如下语句

 //这边演示不使用雪花id因为雪花id很难在演示中展示所以使用订单编号进行演示格式:yyyyMMddHHmmss+new Random().Next(0,10000).ToString().PadLeft(4,'0') var dateTime = new DateTime(2021, 11, 1); var order = await _myDbContext.Set().Where(o => o.OrderNo== 202112201900001111&&o.CreateTime< dateTime).FirstOrDefaultAsync();

上述语句OrderNo会查询Order_202112这张表,然后时间索引会查询......Order_202108、Order_202109、Order_202110,然后两者取一个交集我们发现其实是没有结果的,这个时候应该是返回默认值null或者直接报错

这就是一个简单的原理

直接开始

接下来我将用订单编号和创建时间来为大演示,数据库采用sqlserver(你也可以换成任意efcore支持的数据库),其中编号格式yyyyMMddHHmmss+new Random().Next(0,10000).ToString().PadLeft(4,'0'),创建时间是DateTime格式并且创建时间按月分表,这边不采用雪花id是因为雪花id的实现会根据workid和centerid的不一样而出现不一样的效果,接下来我们通过简单的5步操作实现多字段分片

添加依赖

首先我们添加两个依赖,一个是ShardingCore一个EFCore.SqlServer

 //请安装最新版本目前x.3.2.x+,第一个版本号6代表efcore的版本号 Install-Package ShardingCore -Version 6.3.2 Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 6.0.1

创建一个订单对象

 public class Order { public string Id { get; set; } public string OrderNo { get; set; } public string Name { get; set; } public DateTime CreateTime { get; set; } }

创建DbContext

这边就简单的创建了一个dbcontext,并且设置了一下order如何映射到数据库,当然你可以采用attribute的方式而不是一定要fluentapi

 ///  /// 如果需要支持分表必须要实现 ///  public class DefaultDbContext:AbstractShardingDbContext,IShardingTableDbContext { public DefaultDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity(o => { o.HasKey(p => p.Id); o.Property(p => p.OrderNo).IsRequired().HasMaxLength(128).IsUnicode(false); o.Property(p => p.Name).IsRequired().HasMaxLength(128).IsUnicode(false); o.ToTable(nameof(Order)); }); } public IRouteTail RouteTail { get; set; } }

创建分片路由

这边我们采用订单创建时间按月分表

 public class OrderVirtualRoute : AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute { ///  /// 配置主分表字段是CreateTime,额外分表字段是OrderNo ///  ///  public override void Configure(EntityMetadataTableBuilder builder) { builder.ShardingProperty(o => o.CreateTime); builder.ShardingExtraProperty(o => o.OrderNo); } ///  /// 是否要在程序运行期间自动创建每月的表 ///  ///  public override bool AutoCreateTableByTime() { return true; } ///  /// 分表从何时起创建 ///  ///  public override DateTime GetBeginTime() { return new DateTime(2021, 9, 1); } ///  /// 配置额外分片路由规则 ///  ///  ///  ///  ///  public override Expression> GetExtraRouteFilter(object shardingKey, ShardingOperatorEnum shardingOperator, string shardingPropertyName) { switch (shardingPropertyName) { case nameof(Order.OrderNo): return GetOrderNoRouteFilter(shardingKey, shardingOperator); default: throw new NotImplementedException(shardingPropertyName); } } ///  /// 订单编号的路由 ///  ///  ///  ///  private Expression> GetOrderNoRouteFilter(object shardingKey, ShardingOperatorEnum shardingOperator) { //将分表字段转成订单编号 var orderNo = shardingKey?.ToString() ?? string.Empty; //判断订单编号是否是我们符合的格式 if (!CheckOrderNo(orderNo, out var orderTime)) { //如果格式不一样就直接返回false那么本次查询因为是and链接的所以本次查询不会经过任何路由,可以有效的防止恶意攻击 return tail => false; } //当前时间的tail var currentTail = TimeFormatToTail(orderTime); //因为是按月分表所以获取下个月的时间判断id是否是在临界点创建的 var nextMonthFirstDay = ShardingCoreHelper.GetNextMonthFirstDay(DateTime.Now); if (orderTime.AddSeconds(10) > nextMonthFirstDay) { var nextTail = TimeFormatToTail(nextMonthFirstDay); return DoOrderNoFilter(shardingOperator, orderTime, currentTail, nextTail); } //因为是按月分表所以获取这个月月初的时间判断id是否是在临界点创建的 if (orderTime.AddSeconds(-10) < ShardingCoreHelper.GetCurrentMonthFirstDay(DateTime.Now)) { //上个月tail var previewTail = TimeFormatToTail(orderTime.AddSeconds(-10)); return DoOrderNoFilter(shardingOperator, orderTime, previewTail, currentTail); } return DoOrderNoFilter(shardingOperator, orderTime, currentTail, currentTail); } private Expression> DoOrderNoFilter(ShardingOperatorEnum shardingOperator, DateTime shardingKey, string minTail, string maxTail) { switch (shardingOperator) { case ShardingOperatorEnum.GreaterThan: case ShardingOperatorEnum.GreaterThanOrEqual: { return tail => String.Compare(tail, minTail, StringComparison.Ordinal) >= 0; } case ShardingOperatorEnum.LessThan: { var currentMonth = ShardingCoreHelper.GetCurrentMonthFirstDay(shardingKey); //处于临界值 o=>o.time < [2021-01-01 00:00:00] 尾巴20210101不应该被返回 if (currentMonth == shardingKey) return tail => String.Compare(tail, maxTail, StringComparison.Ordinal) < 0; return tail => String.Compare(tail, maxTail, StringComparison.Ordinal) <= 0; } case ShardingOperatorEnum.LessThanOrEqual: return tail => String.Compare(tail, maxTail, StringComparison.Ordinal) <= 0; case ShardingOperatorEnum.Equal: { var isSame = minTail == maxTail; if (isSame) { return tail => tail == minTail; } else { return tail => tail == minTail || tail == maxTail; } } default: { return tail => true; } } } private bool CheckOrderNo(string orderNo, out DateTime orderTime) { //yyyyMMddHHmmss+new Random().Next(0,10000).ToString().PadLeft(4,'0') if (orderNo.Length == 18) { if (DateTime.TryParseExact(orderNo.Substring(0, 14), "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parseDateTime)) { orderTime = parseDateTime; return true; } } orderTime = DateTime.MinValue; return false; } }

这边我来讲解一下为什么用额外字段分片需要些这么多代码呢,其实是这样的因为你是用订单创建时间CreateTime来进行分片的那么CreateTimeOrderNo的赋值原理上说应该在系统里面是不可能实现同一时间赋值的肯定有先后关系可能是几微妙甚至几飞秒,但是为了消除这种差异这边采用了临界点兼容算法来实现,让我们来看下一下代码

 var order=new Order() //执行这边生成出来的id是2021-11-30 23:59:59.999.999 order.OrderNo=DateTime.Now.ToString("yyyyMMddHHmmss")+"xxx"; //business code //具体执行时间不确定,哪怕没有business code也没有办法保证两者生成的时间一致,当然如果你可以做到一致完全不需要这么复杂的编写 ............ //执行这边生成出来的时间是2021-12-01 00:00:00.000.000 order.CreateTime=DateTime.Now;

当然系统里面采用了前后添加10秒是一个比较保守的估算你可以采用前后一秒甚至几百毫秒都是ok的,具体业务具体实现,因为大部分的创建时间可能是由框架在提交后才会生成而不是new Order的时候,当然也不排除这种情况,当然如果你只需要考虑equal一种情况可以只编写equal的判断而不需要全部情况都考虑

ShardingCore启动配置

 ILoggerFactory efLogger = LoggerFactory.Create(builder => { builder.AddFilter((category, level) => category == DbLoggerCategory.Data
                
                

-六神源码网