.Net下你不得不看的分表分库解决方案-多字段分片
.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或者直接报错
这就是一个简单的原理
直接开始
接下来我将用订单编号和创建时间来为大演示,数据库采用sqlserve(你也可以换成任意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
/// <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>
public 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;
}
//当前时间的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<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
来进行分片的那么CreateTime
和OrderNo
的赋值原理上说应该在系统里面是不可能实现同一时间赋值的肯定有先后关系可能是几微妙甚至几飞秒,但是为了消除这种差异这边采用了临界点兼容算法来实现,让我们来看下一下代码
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--------------");
//订单号和创建时间查询 精确定位 无路由结果 抛错或者返回default
Console.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下你不得不看的分表分库解决方案-多字段分片的更多相关文章
- efcore在Saas系统下多租户零脚本分表分库读写分离解决方案
efcore在Saas系统下多租户零脚本分表分库读写分离解决方案 ## 介绍 本文ShardinfCore版本x.6.0.20+ 本期主角: - [`ShardingCore`](https://gi ...
- 分表分库解决方案(mycat,tidb,shardingjdbc)
公司最近有分表分库的需求,所以整理一下分表分库的解决方案以及相关问题. 1.sharding-jdbc(sharding-sphere) 优点: 1.可适用于任何基于java的ORM框架,如:JPA. ...
- .Net 下高性能分表分库组件-连接模式原理
ShardingCore ShardingCore 一款ef-core下高性能.轻量级针对分表分库读写分离的解决方案,具有零依赖.零学习成本.零业务代码入侵. Github Source Code 助 ...
- efcore使用ShardingCore实现分表分库下的多租户
efcore使用ShardingCore实现分表分库下的多租户 介绍 本期主角:ShardingCore 一款ef-core下高性能.轻量级针对分表分库读写分离的解决方案,具有零依赖.零学习成本.零业 ...
- Furion分表分库我也要happy coding
Furion分表分库集成ShardingCore ShardingCore ShardingCore 易用.简单.高性能.普适性,是一款扩展针对efcore生态下的分表分库的扩展解决方案,支持efco ...
- .Net分表分库动态化处理
介绍 本期主角:ShardingCore 一款ef-core下高性能.轻量级针对分表分库读写分离的解决方案,具有零依赖.零学习成本.零业务代码入侵 背景 最近有个小伙伴来问我,分表下他有一批数据,这个 ...
- .Net下极限生产力之efcore分表分库全自动化迁移CodeFirst
.Net下极限生产力之分表分库全自动化Migrations Code-First ## 介绍 本文ShardinfCore版本x.6.x.x+ 本期主角: - [`ShardingCore`](htt ...
- NetCore框架WTM的分表分库实现
介绍 本期主角: ShardingCore 一款ef-core下高性能.轻量级针对分表分库读写分离的解决方案,具有零依赖.零学习成本.零业务代码入侵 WTM WalkingTec.Mvvm框架(简称W ...
- 总结下Mysql分表分库的策略及应用
上月前面试某公司,对于mysql分表的思路,当时简要的说了下hash算法分表,以及discuz分表的思路,但是对于新增数据自增id存放的设计思想回答的不是很好(笔试+面试整个过程算是OK过了,因与个人 ...
随机推荐
- 日常Java 2021/9/26 (二柱升级版)
package m; import java.util.Scanner;import java.util.Random; public class di_er { static int number= ...
- 关于redis HSCAN count参数不生效的问题
这的确是个坑,HSCAN是为了处理大量数据而设计的,可能也是因为这个原因,在数据量较少的情况下count参数并不会生效,具体阈值是多少并没有实际测验过不过可以断定的是一百条数据一下估计是不会生效的.
- doy05循环语法学习笔记
doy05循环语法学习笔记 一.while循环语法: 1.基本用法示例 x = 1 while x <= 5: print(x) x += 1 2.死循环:永远不结束的循环 如:while Tr ...
- 零基础学习java------day7------面向对象
1. 面向对象 1.1 概述 面向过程:c语言 面向对象:java :python:C++等等 面向对象的概念: (万物皆对象)------think in java everything in ...
- 淘宝、网易移动端 px 转换 rem 原理,Vue-cli 实现 px 转换 rem
在过去的一段时间里面一直在使用Vue配合 lib-flexible和px2rem-loader配合做移动端的网页适配.秉着求知的思想,今天决定对他的原理进行分析.目前网上比较主流使用的就是淘宝方 ...
- 学习Vue源码前的几项必要储备(二)
7项重要储备 Flow 基本语法 发布/订阅模式 ES6+ 语法 原型链.闭包 函数柯里化 event loop 接上讲 聊到了ES6的几个重要语法,加下来到第四点继续开始. 4.原型链.闭包 原型链 ...
- windows Notepad++ 上配置 vs 编译器 , 编译并运行
windows 中 配置 vs编译器 在Linux下,Kris是倾向于在终端中使用gcc和g++来编译C/C++的,在Windows下相信很多人都是选择臃肿的Visual Studio,我亦不免如此. ...
- c学习 - 第八章:函数
8.7 数组作函数的参数 1.数组元素作函数的参数--值传递,单向传递 2.数组名做函数的参数--地址传送 (1)实参:数组名做实参,传递的是数组首元素的地址 (2)形参:使用同类型的数组名或指针变量 ...
- Maven错误收集
Eclipse 创建项目时报错 Could not resolve archetype org.apache.maven.archetypes:maven-archetype-quickstart:1 ...
- Actuator监控器
一.简介 Actuator(激励者;执行器)是Spring Boot提供的一个可挺拔模块,用于对工程进行监控.其通过不同的监控终端实现不同的监控功能.其功能与Dubbo的监控中心类似,不同的是,Dub ...