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

介绍

本期主角: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

fe89ef31ced6ee61271665acf355ca18.png

cfef00b10110fc61beece1b93cb9e33a.png

创建一个订单对象

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();}

测试结果
55a038634011148966c8106ae0d793c1.png

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

测试无路由返回默认值

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

我们再次来看下测试结果
6f31f633c1339b03c7a3e18fd3ef4bd6.png

为何我们测试是不经过数据库直接查询,原因就是在我们做各个属性分片交集的时候返回了空那么框架会选择抛出异常或者返回默认值两种选项,并且我们在编写路由的时候判断格式不正确返回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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/293588.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

知乎高赞:看懂这个颠覆世界观的认知,远比做1000道题更有用!

▲ 点击查看知乎上曾有个提问&#xff1a;“见过世面究竟有多重要&#xff1f;”其中一个点赞过万回答让无数网友产生共鸣&#xff1a;会讲究&#xff0c;能将就&#xff0c;能享受最好的&#xff0c;也能承受最坏的。见过世面的他们自然会在人群中散发不一样的气质&#xff0c…

WebBrowser!

WebBrowser! 原文:WebBrowser!我现在先放一些基础的文章在这里&#xff0c;以后再放别的上来官方范例连接http://www.microsoft.com/china/msdn/library/langtool/vcsharp/OvervwWebBrowExp.mspxQ&A 2005年5月21日 0:14:19 Q: 新键入的地址不能在新建好的窗口里打开:A:每…

Android USB Host与HID通讯(二)

2019独角兽企业重金招聘Python工程师标准>>> 原文出处&#xff1a;http://han21912.lofter.com/post/c3919_51401d 接上一篇&#xff1a;Android USB Host与HID通讯 (一) 从上篇已经可以枚举到HID设备&#xff0c;接下来看看寻找设备的接口和通信端点&#xff0c;…

CentOS7安装PHP5.6.23

为什么80%的码农都做不了架构师&#xff1f;>>> 美国时间2014年11月13日&#xff0c;PHP开发团队&#xff0c;在「PHP 5.6.3 is available&#xff5c;PHP: Hypertext Preprocessor」上公布了PHP5.6系的最新版本「PHP 5.6.3」。 在最新的版本5.6.3不仅修改了多个Bu…

为什么接吻需要闭眼睛?

1 你用上5G了吗&#xff1f;它已经用上了▼2 戴口罩的好处又增加了▼3 原来如此...▼4 哈哈哈哈▼5 露脐装的正确打开方式&#xff08;素材源于网络&#xff0c;侵删&#xff09;▼6 火鸡面到底有多辣▼7 孩子你要完了&#xff08;素材来源网络&#xff0c;侵删&#xff0…

最近要出绩效了

上周我们公司的绩效面谈全部结束了&#xff0c;每年到这个时间点就是打绩效的时候了&#xff0c;对于职场打工人来说绩效绝对是最重要的事情之一&#xff0c;原因也很简单&#xff1a;奖金、晋升、涨薪都和它有关系。比如下面这个美团员工在脉脉上的自曝就很凄凉&#xff1a;互…

从B 树、B+ 树、B* 树谈到R 树

作者&#xff1a;July、weedge、Frankie。编程艺术室出品。 说明&#xff1a;本文从B树开始谈起&#xff0c;然后论述B树、B*树&#xff0c;最后谈到R 树。其中B树、B树及B*树部分由weedge完成&#xff0c;R 树部分由Frankie完成&#xff0c;全文最终由July统稿修订完成。 出…

纯IPv6环境App适配的坑

来源&#xff1a;伯乐在线专栏作者 - MrPeak 链接&#xff1a;http://ios.jobbole.com/86580/ 苹果从2016年6月1号开始&#xff0c;强制所有app必须支持纯IPv6的网络环境。这项举措将对IPv6的普及起到一定的推动作用&#xff0c;也体现了Apple作为国际大厂的担当。 大部分App由…

使用Visual Studio 创建新的Web Part项目

使用Visual Studio 创建新的Web Part项目 Web Part是你将为SharePoint创建的最常见的对象之一。它是平台构建的核心基块。1. 管理员身份打开Visual Studio&#xff0c;新建空白SharePoint项目。命名WroxSPProject&#xff0c;点击确定。部署为场解决方案&#xff0c;点击完成。…

聊一聊Yarp结合Nacos完成服务发现

背景 Yarp 这个反向代理出来后&#xff0c;相信还是有不少人在关注的。在 Yarp 中&#xff0c;反向代理的配置默认也是基于配置文件的&#xff0c;也有不少大佬已经把这个配置做成了数据库配置可视化界面。仔细想了想&#xff0c;做成数据库配置&#xff0c;好像只是便于配置的…

相亲对象能有多油腻......

1 冰起来再烧&#xff08;素材来源网络&#xff0c;侵删&#xff09;▼2 还没从年假清醒的我&#xff08;素材来源网络&#xff0c;侵删&#xff09;▼3 孙悟空为啥没被人收去当坐骑&#xff08;素材来源网络&#xff0c;侵删&#xff09;▼4 成年人的潜台词&#xff08;素材…

教你如何迅速秒杀掉:99%的海量数据处理面试题

教你如何迅速秒杀掉&#xff1a;99%的海量数据处理面试题 作者&#xff1a;July 出处&#xff1a;结构之法算法之道blog 前言 一般而言&#xff0c;标题含有“秒杀”&#xff0c;“99%”&#xff0c;“史上最全/最强”等词汇的往往都脱不了哗众取宠之嫌&#xff0c;但进一步来讲…

【.NET 遇上 GraphQL】 ChilliCream 平台介绍

ChilliCreamhttps://chillicream.com/https://github.com/ChilliCream/hotchocolateChilliCream 平台包含了四个产品, Hot Chocolate, Banana Cake Pop, Strawberry Shake, 平台提供开发人员工具和服务以加快整个开发过程。Hot ChocolateHot Chocolate 是 .NET 平台下的一个开源…

.NET 的 WebSocket 开发包比较

编者按 本文出现在第三方产品评论部分中。在这一部分的文章只提供给会员&#xff0c;不允许工具供应商用来以任何方式和形式来促销或宣传产品。请会员报告任何垃圾信息或广告。 Web项目常常需要将数据尽可能快地推送给客户&#xff0c;必要时无需等待客户端请求。对于与用户之间…

《CMake实践》笔记二:INSTALL/CMAKE_INSTALL_PREFIX

《CMake实践》笔记一&#xff1a;PROJECT/MESSAGE/ADD_EXECUTABLE 《CMake实践》笔记二&#xff1a;INSTALL/CMAKE_INSTALL_PREFIX 《CMake实践》笔记三&#xff1a;构建静态库与动态库 及 如何使用外部共享库和头文件 四、更好一点的Hello World 没有最好&#xff0c;只有更好…

14年前,林国强院士发现自己学生论文无法重复后,是这样处理的

全世界只有3.14 % 的人关注了爆炸吧知识本文转自&#xff1a;iNature让我们把时间拨回至 2007 年 3 月&#xff0c;当年&#xff0c;一封以林国强院士的名义发表的公开信&#xff0c;在网络上流传。信中披露&#xff0c;林院士发现自己的一名博士生发表在权威期刊《美国化学会志…

支持向量机通俗导论(理解SVM的三层境界)

支持向量机通俗导论&#xff08;理解SVM的三层境界&#xff09; 作者&#xff1a; July &#xff1b; 致谢&#xff1a; pluskid、 白石、J erryLead。出处&#xff1a;结构之法算法之道 blog 。 前言 动笔写这个支持向量机(support vector machine)是费了不少劲和困难的&#…

WPF 实现加速小火箭~

WPF开发者QQ群&#xff1a; 340500857由于微信群人数太多入群请添加小编微信号yanjinhuawechat 或 W_Feng_aiQ 邀请入群需备注WPF开发者 PS&#xff1a;有更好的方式欢迎推荐。01—代码如下一、创建 SpeedRocketsExample.xaml 代码如下。<Window x:Class"WPFDevelopers…

最懂男人心的内裤,戳100个洞透气,超舒服

▲ 点击查看有什么日用品&#xff0c;需要盆友们时不时就经常补货买买买的&#xff1f;除了家里的酱油、纸巾、沐浴露&#xff0c;还有一样hin重要&#xff0c;那就是内裤&#xff01;除了洗澡的空挡&#xff0c;内裤就一直穿在身上&#xff0c;可以说内裤是比女友还亲、比手机…

Logback也爆漏洞了,总结下最近log相关的几个漏洞

大家好&#xff0c;我是君哥&#xff0c;周末又要结束了。前些天 Apache Log4j2 接连报了几个重大漏洞&#xff0c;好在我们的系统使用的 logback&#xff0c;可当我们正庆幸的时候&#xff0c;logback 也爆出漏洞了。今天我们一起来看一下这几个漏洞。CVE-2021-42550先看一下官…