介绍

本期主角: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<Order>().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.2Install-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

/// <summary>/// 如果需要支持分表必须要实现<see cref="IShardingTableDbContext"/>/// </summary>public class DefaultDbContext:AbstractShardingDbContext,IShardingTableDbContext{public DefaultDbContext(DbContextOptions options) : base(options){}protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.Entity<Order>(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<Order>{/// <summary>/// 配置主分表字段是CreateTime,额外分表字段是OrderNo/// </summary>/// <param name="builder"></param>public override void Configure(EntityMetadataTableBuilder<Order> builder){builder.ShardingProperty(o => o.CreateTime);builder.ShardingExtraProperty(o => o.OrderNo);}/// <summary>/// 是否要在程序运行期间自动创建每月的表/// </summary>/// <returns></returns>public override bool AutoCreateTableByTime(){return true;}/// <summary>/// 分表从何时起创建/// </summary>/// <returns></returns>public override DateTime GetBeginTime(){return new DateTime(2021, 9, 1);}/// <summary>/// 配置额外分片路由规则/// </summary>/// <param name="shardingKey"></param>/// <param name="shardingOperator"></param>/// <param name="shardingPropertyName"></param>/// <returns></returns>public override Expression<Func<string, bool>> GetExtraRouteFilter(object shardingKey, ShardingOperatorEnum shardingOperator, string shardingPropertyName){switch (shardingPropertyName){case nameof(Order.OrderNo): return GetOrderNoRouteFilter(shardingKey, shardingOperator);default: throw new NotImplementedException(shardingPropertyName);}}/// <summary>/// 订单编号的路由/// </summary>/// <param name="shardingKey"></param>/// <param name="shardingOperator"></param>/// <returns></returns>private Expression<Func<string, bool>> GetOrderNoRouteFilter(object shardingKey,ShardingOperatorEnum shardingOperator){//将分表字段转成订单编号var orderNo = shardingKey?.ToString() ?? string.Empty;//判断订单编号是否是我们符合的格式if (!CheckOrderNo(orderNo, out var orderTime)){//如果格式不一样就直接返回false那么本次查询因为是and链接的所以本次查询不会经过任何路由,可以有效的防止恶意攻击return tail => false;}//当前时间的tailvar 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)){//上个月tailvar previewTail = TimeFormatToTail(orderTime.AddSeconds(-10));return DoOrderNoFilter(shardingOperator, orderTime, previewTail, currentTail);}return DoOrderNoFilter(shardingOperator, orderTime, currentTail, currentTail);}private Expression<Func<string, bool>> 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.Database.Command.Name && level == LogLevel.Information).AddConsole();
});
var builder = WebApplication.CreateBuilder(args);// Add services to the container.builder.Services.AddControllers();
builder.Services.AddShardingDbContext<DefaultDbContext>((conStr,builder)=>builder.UseSqlServer(conStr).UseLoggerFactory(efLogger)).Begin(o =>{o.CreateShardingTableOnStart = true;o.EnsureCreatedWithOutShardingTable = true;}).AddShardingTransaction((connection, builder) =>{builder.UseSqlServer(connection).UseLoggerFactory(efLogger);}).AddDefaultDataSource("ds0","Data Source=localhost;Initial Catalog=ShardingMultiProperties;Integrated Security=True;")//如果你是sqlserve只需要修改这边的链接字符串即可.AddShardingTableRoute(op =>{op.AddShardingTableRoute<OrderVirtualRoute>();}).AddTableEnsureManager(sp=>new SqlServerTableEnsureManager<DefaultDbContext>())//告诉ShardingCore启动时有哪些表.End();var app = builder.Build();// Configure the HTTP request pipeline.
app.Services.GetRequiredService<IShardingBootstrapper>().Start();app.UseAuthorization();app.MapControllers();//额外添加一些种子数据
using (var serviceScope = app.Services.CreateScope())
{var defaultDbContext = serviceScope.ServiceProvider.GetService<DefaultDbContext>();if (!defaultDbContext.Set<Order>().Any()){var orders = new List<Order>(8);var beginTime = new DateTime(2021, 9, 5);for (int i = 0; i < 8; i++){var orderNo = beginTime.ToString("yyyyMMddHHmmss") + i.ToString().PadLeft(4, '0');orders.Add(new Order(){Id = Guid.NewGuid().ToString("n"),CreateTime = beginTime,Name = $"Order" + i,OrderNo = orderNo});beginTime = beginTime.AddDays(1);if (i % 2 == 1){beginTime = beginTime.AddMonths(1);}}defaultDbContext.AddRange(orders);defaultDbContext.SaveChanges();}
}
app.Run();

整个配置下来其实也就两个地方需要配置还是相对比较简单的,直接启动开始我们的测试模式

测试

默认配置下的测试

public async Task<IActionResult> Test1(){ //订单名称全表扫描Console.WriteLine("--------------Query Name Begin--------------");var order1 = await _defaultDbContext.Set<Order>().Where(o=>o.Name=="Order3").FirstOrDefaultAsync();Console.WriteLine("--------------Query Name End--------------");//订单编号查询 精确定位Console.WriteLine("--------------Query OrderNo Begin--------------");var order2 = await _defaultDbContext.Set<Order>().Where(o=>o.OrderNo== "202110080000000003").FirstOrDefaultAsync();Console.WriteLine("--------------Query OrderNo End--------------");//创建时间查询 精确定位Console.WriteLine("--------------Query OrderCreateTime Begin--------------");var dateTime = new DateTime(2021,10,08);var order4 = await _defaultDbContext.Set<Order>().Where(o=>o.CreateTime== dateTime).FirstOrDefaultAsync();Console.WriteLine("--------------Query OrderCreateTime End--------------");//订单编号in 精确定位Console.WriteLine("--------------Query OrderNo Contains Begin--------------");var orderNos = new string[] { "202110080000000003", "202111090000000004" };var order5 = await _defaultDbContext.Set<Order>().Where(o=> orderNos.Contains(o.OrderNo)).ToListAsync();Console.WriteLine("--------------Query OrderNo Contains End--------------");//订单号和创建时间查询 精确定位 无路由结果 抛错或者返回defaultConsole.WriteLine("--------------Query OrderNo None Begin--------------");var time = new DateTime(2021,11,1);var order6 = await _defaultDbContext.Set<Order>().Where(o=> o.OrderNo== "202110080000000003"&&o.CreateTime> time).FirstOrDefaultAsync();Console.WriteLine("--------------Query OrderNo None End--------------");//非正确格式订单号 抛错或者返回default防止击穿数据库Console.WriteLine("--------------Query OrderNo Not Check Begin--------------");var order3 = await _defaultDbContext.Set<Order>().Where(o => o.OrderNo == "a02110080000000003").FirstOrDefaultAsync();Console.WriteLine("--------------Query OrderNo Not Check End--------------");return Ok();}

测试结果

测试结果非常完美除了无法匹配路由的时候那么我们该如何设置呢

测试无路由返回默认值

builder.Services.AddShardingDbContext<DefaultDbContext>(...).Begin(o =>{
....o.ThrowIfQueryRouteNotMatch = false;//配置默认不抛出异常})

我们再次来看下测试结果

为何我们测试是不经过数据库直接查询,原因就是在我们做各个属性分片交集的时候返回了空那么框架会选择抛出异常或者返回默认值两种选项,并且我们在编写路由的时候判断格式不正确返回return tail => false;直接让所有的交集都是空所以不会进行一次无意义的数据库查询

总结

看到这边你应该已经看到了本框架的强大之处,本框架不但可以实现多字段分片还可以实现自定义分片,而不是单单按时间分片这么简单,我完全可以设置订单从2021年后的订单按月分片,2021年前的订单按年分片,对于sharding-core而言这简直轻而易举,但是据我所知.Net下目前除了我没有任何一款框架可以做到真正的全自动分片+多字段分片,所以我们在设计框架分片的时候尽可能的将有用的信息添加到一些无意义的字段上比如Id可以有效的解决很多在大数据下发生的问题,你可以简单理解为我加了一个索引并且附带了额外列,我加了一个id并且带了分表信息在里面,也可以完全设计出一款附带分库的属性到id里面使其可以支持分表分库

最后的最后

demo地址 https://github.com/xuejmnet/MultiShardingProperties

您都看到这边了确定不点个star或者赞吗,一款.Net不得不学的分库分表解决方案,简单理解为sharding-jdbc在.net中的实现并且支持更多特性和更优秀的数据聚合,拥有原生性能的97%,并且无业务侵入性,支持未分片的所有efcore原生查询

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

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

.Net下你不得不看的分表分库解决方案-多字段分片相关推荐

  1. 分表分库解决方案(mycat,tidb,shardingjdbc)

    分表分库解决方案(mycat,tidb,shardingjdbc) 参考文章: (1)分表分库解决方案(mycat,tidb,shardingjdbc) (2)https://www.cnblogs. ...

  2. .Net 下高性能分表分库组件-连ShardingCore接模式原理

    ShardingCore 一款ef-core下高性能.轻量级针对分表分库读写分离的解决方案,具有零依赖.零学习成本.零业务代码入侵. Github Source Code 助力dotnet 生态 Gi ...

  3. 软件架构场景之—— 分表分库:单表数据量大读写缓慢如何解决?

    业务背景 一个电商系统的架构优化,该系统中包含用户和订单 2 个主要实体,每个实体涵盖数据量如下表所示 实体 数据量 增长趋势 用户 上千万 每日十万 订单 上亿 每日百万级速度增长,之后可能是千万级 ...

  4. 总结下Mysql分表分库的策略及应用

    上月前面试某公司,对于mysql分表的思路,当时简要的说了下hash算法分表,以及discuz分表的思路, 但是对于新增数据自增id存放的设计思想回答的不是很好(笔试+面试整个过程算是OK过了,因与个 ...

  5. .NETCore 下支持分表分库、读写分离的通用 Repository

    首先声明这篇文章不是标题党,我说的这个类库是 FreeSql.Repository,它作为扩展库现实了通用仓储层功能,接口规范参数 abp vnext,定义和实现基础的仓储层(CURD). 安装 do ...

  6. 学会数据库读写分离、分表分库

    https://www.cnblogs.com/joylee/p/7513038.html 系统开发中,数据库是非常重要的一个点.除了程序的本身的优化,如:SQL语句优化.代码优化,数据库的处理本身优 ...

  7. Abp VNext 集成sharding-core 分表分库

    ShardingCore 易用.简单.高性能.普适性,是一款扩展针对efcore生态下的分表分库的扩展解决方案,支持efcore2+的所有版本,支持efcore2+的所有数据库.支持自定义路由.动态路 ...

  8. [NewLife.XCode]分表分库(百亿级大数据存储)

    NewLife.XCode是一个有15年历史的开源数据中间件,支持netcore/net45/net40,由新生命团队(2002~2019)开发完成并维护至今,以下简称XCode. 整个系列教程会大量 ...

  9. 冷热分离和直接使用大数据库_用读写分离与分表分库解决高访问量和大数据量...

    原标题:用读写分离与分表分库解决高访问量和大数据量 一. 数据切分 关系型数据库本身比较容易成为系统瓶颈,单机存储容量.连接数.处理能力都有限.当单表的数据量达到1000W或100G以后,由于查询维度 ...

最新文章

  1. Django博客系统(首页分类数据展示)
  2. Oracle内部错误ORA-07445:[_memcmp()+88] [SIGSEGV]一例
  3. python 基础知识点整理 和详细应用
  4. MVC的增删改和Razor
  5. Mybatis使用时因jdbcType类型大小写书写不规范导致的异常
  6. 图像处理中消除相机透镜畸变和视角变换
  7. CG CTF WEB Download~!
  8. Veeam在思科2017年合作伙伴峰会上荣获ISV年度最佳合作伙伴全球奖
  9. 7 补充业务_哪些情况可以补充申报?金关账册报核要申报哪些数据?
  10. mac模式怎样构造在jsp中_mac下tomcat的配置和jdk的设置 jsp的初级知识
  11. Hibernate JPA中insert插入数据后自动执行select last_insert_id()解决方法
  12. 加州伯克利本科学计算机好吗,美国加州大学伯克利分校和卡耐基梅隆大学计算机科学CS专业哪个好?...
  13. Apache和Tomcat的区别与联系
  14. 转:面试题收集——Java基础部分(一)
  15. 月薪 1 万和 10 万的人,到底差在哪儿?
  16. Linux学习之路(2-1)文件、目录与磁盘格式
  17. 优秀的孩子是这样培养的
  18. 28.4 kvm介绍 28.5 Centos7上安装KVM 28.6 配置网卡 28.7 创建虚拟机安装CentOS7
  19. 如何html设置下载的字体呢?
  20. 电子学会机器人等级考试三四级考试大纲

热门文章

  1. 正则验证金额大于等于0,并且只到小数点后2位
  2. Android ping命令 -- Runtime
  3. Android两个注意事项.深入了解Intent和IntentFilter(两)
  4. win7 绑定arp
  5. 31天重构学习笔记19. 提取工厂类
  6. 解决vista/win7安装windows live messenger 2011找不到wlidcli.dll及错误800488eb .
  7. 漫水填充及Photoshop中魔术棒选择工具的实现
  8. Windows Server 2008 R2 之二十一远程桌面服务RD之二
  9. es6拼接字符串的方式。
  10. ubuntu scp命令或者用root连接ssh提示:Permission denied, please try again.错误