分库分表下极致的优化
题外话
这边说一句题外话,就是ShardingCore
目前已经正式加入 NCC 开源组织了,也是希望框架和社区能发展的越来越好,希望为更多.netter提供解决方案和开源组件
介绍
依照惯例首先介绍本期主角:ShardingCore
一款ef-core下高性能、轻量级针对分表分库读写分离的解决方案,具有零依赖、零学习成本、零业务代码入侵
dotnet下唯一一款全自动分表,多字段分表框架,拥有高性能,零依赖、零学习成本、零业务代码入侵,并且支持读写分离动态分表分库,同一种路由可以完全自定义的新星组件框架
你的star和点赞是我坚持下去的最大动力,一起为.net生态提供更好的解决方案
项目地址
github地址 https://github.com/dotnetcore/sharding-core
gitee地址 https://gitee.com/dotnetchina/sharding-core
本次优化点
直奔主题来讲下本次的极致优化具体是优化了什么,简单说就是CircuitBreaker
和FastFail
.
断路器CircuitBreaker
我们假设这么一个场景,现在我们有一个订单order表,订单会按照月份进行分片,那么订单表会有如下几个order_202201
、order_202202
、order_202203
、order_202204
、order_202205
,假设我们有5张表。
首先我们来看一条普通的语句
select * from order where id='xxx' limit 1
这是一条普通的不能在普通的sql了,查询第一条id是xxx的订单,
那么他在分表下面会如何运行
//开启5个线程并发查询
select * from order_202201 where id='xxx' limit 1
select * from order_202202 where id='xxx' limit 1
select * from order_202203 where id='xxx' limit 1
select * from order_202204 where id='xxx' limit 1
select * from order_202205 where id='xxx' limit 1
//查询出来的结果在内存中进行聚合成一个list集合
//然后在对这个list集合进行第一条的获取
list.Where(o=>o is not null).FirstOrDefault()
这个操作我相信很多同学都是可以了解的,稍微熟悉点分表分库的同学应该都知道这是基本操作了,但是这个操作看似高效(时间上)但是在连接数上而言并不是那么的高效,因为同一时间需要开打的连接数将由5个
那么在这个背景下ShardingCore
参考ShardingSphere 提供了更加友好的连接控制和内存聚合模式ConnectionMode
这个张图上我们可以清晰的看到不同的数据库直接才用了一个并发限制,比如设置的是2,那么在相同库里面的查询将是每2个一组,进行查询,这样可以控制在同一个数据库下的连接数,进而解决了客户端连接模式下的连接数消耗猛烈的一个弊端。
//开启5个线程并发查询
{//并行select * from order_202201 where id='xxx' limit 1select * from order_202202 where id='xxx' limit 1
}//串行
{//并行select * from order_202203 where id='xxx' limit 1select * from order_202204 where id='xxx' limit 1
}//串行
{select * from order_202205 where id='xxx' limit 1
}
//查询出来的结果在内存中进行聚合成一个list集合
//然后在对这个list集合进行第一条的获取
list.Where(o=>o is not null).FirstOrDefault()
到目前为止这边已经对分片的查询优化到了一个新的高度。但是虽然我们优化了连接数的处理,但是就查询速度而言基本上是没有之前的那么快,可以说和你分组的组数成线性增加时间的消耗。
所以到此为止ShardingCore
又再一次进化
出了全新的翅膀CircuitBreaker
断路器,我们继续往下看
我们现在的sql是
select * from order where id='xxx' limit 1
那么如果我们针对这个sql进行优化呢,譬如
select * from order where id='xxx' order by create_time desc limit 1
同样是查询第一条,添加了一个order排序那么情况就会大大的不一样,首先我们来观察我们的分片查询
//开启5个线程并发查询
-- select * from order_202201 where id='xxx' order by create_time desc limit 1
-- select * from order_202202 where id='xxx' order by create_time desc limit 1
-- select * from order_202203 where id='xxx' order by create_time desc limit 1
-- select * from order_202204 where id='xxx' order by create_time desc limit 1
-- select * from order_202205 where id='xxx' order by create_time desc limit 1
-- 抛弃上述写法select * from order_202205 where id='xxx' order by create_time desc limit 1select * from order_202204 where id='xxx' order by create_time desc limit 1select * from order_202203 where id='xxx' order by create_time desc limit 1select * from order_202202 where id='xxx' order by create_time desc limit 1select * from order_202201 where id='xxx' order by create_time desc limit 1
如果在连接模式下那么他们将会是2个一组,那么我们在查询第一组的结果后是否就可以直接抛弃掉下面的所有查询,也就是我们只需要查询
select * from order_202205 where id='xxx' order by create_time desc limit 1select * from order_202204 where id='xxx' order by create_time desc limit 1
只要他们是有返回一个以上的数据那么本次分片查询将会被终止,ShardingCore
目前的大杀器,本来年前已经开发完成了,奈何太懒只是发布了版本并没有相关的说明和使用方法
CircuitBreaker
断路器,它具有类似拉闸中断操作的功能,这边简单说下linq操作下的部分方法的断路器点在哪里
方法名 | 是否支持中断操作 | 中断条件 |
---|---|---|
First | 支持 | 按顺序查询到第一个时就可以放弃其余查询 |
FirstOrDefault | 支持 | 按顺序查询到第一个时就可以放弃其余查询 |
Last | 支持 | 按顺序倒叙查询到第一个时就可以放弃其余查询 |
LastOrDefault | 支持 | 按顺序倒叙查询到第一个时就可以放弃其余查询 |
Single | 支持 | 查询到两个时就可以放弃,因为元素个数大于1个了需要抛错 |
SingleOrDefault | 支持 | 查询到两个时就可以放弃,因为元素个数大于1个了需要抛错 |
Any | 支持 | 查询一个结果true就可以放弃其余查询 |
All | 支持 | 查询到一个结果fasle就可以放弃其余查询 |
Contains | 支持 | 查询一个结果true就可以放弃其余查询 |
Count | 不支持 | -- |
LongCount | 不支持 | -- |
Max | 支持 | 按顺序最后一条并且查询最大字段是分片顺序同字段是,max的属性只需要查询一条记录 |
Min | 支持 | 按顺序第一条并且查询最小字段是分片顺序同字段,min的属性只需要查询一条记录 |
Average | 不支持 | -- |
Sum | 不支持 | -- |
这边其实只有三个操作是任何状态下都可以支持中断,其余操作需要在额外条件顺序查询的情况下才可以,并且我们本次查询分片涉及到过多的后缀表那么性能和资源的利用将会大大提升
查询配置
废话不多说我们开始以mysql作为本次案例(不要问我为什么不用SqlServer,因为写文章的时候我是mac电脑),这边我们创建一个项目新建一个订单按月分表
新建项目
安装依赖
添加订单表和订单表映射
public class Order{public string Id { get; set; }public string Name { get; set; }public DateTime Createtime { get; set; }}public class OrderMap : IEntityTypeConfiguration<Order>{public void Configure(EntityTypeBuilder<Order> builder){builder.HasKey(o => o.Id);builder.Property(o => o.Id).HasMaxLength(32).IsUnicode(false);builder.Property(o => o.Name).HasMaxLength(255);builder.ToTable(nameof(Order));}}
添加DbContext
public class ShardingDbContext:AbstractShardingDbContext,IShardingTableDbContext{public ShardingDbContext(DbContextOptions<ShardingDbContext> options) : base(options){}protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.ApplyConfiguration(new OrderMap());}public IRouteTail RouteTail { get; set; }}
添加订单分片路由
从5月份开始按创建时间建表
public class OrderRoute:AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute<Order>{public override void Configure(EntityMetadataTableBuilder<Order> builder){builder.ShardingProperty(o => o.Createtime);}public override bool AutoCreateTableByTime(){return true;}public override DateTime GetBeginTime(){return new DateTime(2021, 5, 1);}}
启动配置
简单的配置启动创建表和库,并且添加种子数据
ILoggerFactory efLogger = LoggerFactory.Create(builder =>
{builder.AddFilter((category, level) => category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information).AddConsole();
});
builder.Services.AddControllers();
builder.Services.AddShardingDbContext<ShardingDbContext>().AddEntityConfig(op =>{op.CreateShardingTableOnStart = true;op.EnsureCreatedWithOutShardingTable = true;op.AddShardingTableRoute<OrderRoute>();op.UseShardingQuery((conStr, b) =>{b.UseMySql(conStr, new MySqlServerVersion(new Version())).UseLoggerFactory(efLogger);});op.UseShardingTransaction((conn, b) =>{b.UseMySql(conn, new MySqlServerVersion(new Version())).UseLoggerFactory(efLogger);});}).AddConfig(op =>{op.ConfigId = "c1";op.AddDefaultDataSource("ds0", "server=127.0.0.1;port=3306;database=db2;userid=root;password=root;");op.ReplaceTableEnsureManager(sp=>new MySqlTableEnsureManager<ShardingDbContext>());}).EnsureConfig();
var app = builder.Build();app.Services.GetRequiredService<IShardingBootstrapper>().Start();
using (var scope=app.Services.CreateScope())
{var shardingDbContext = scope.ServiceProvider.GetRequiredService<ShardingDbContext>();if (!shardingDbContext.Set<Order>().Any()){var begin = new DateTime(2021, 5, 2);List<Order> orders = new List<Order>(8);for (int i = 0; i < 8; i++){orders.Add(new Order(){Id = i.ToString(),Name = $"{begin:yyyy-MM-dd HH:mm:ss}",Createtime = begin});begin = begin.AddMonths(1);}shardingDbContext.AddRange(orders);shardingDbContext.SaveChanges();}
}
app.UseAuthorization();
app.MapControllers();
app.Run();
这边默认连接模式的分组是Environment.ProcessorCount
编写查询
没有配置的情况下那么这个查询将是十分糟糕
接下来我们将配置Order的查询
public class OrderQueryConfiguration:IEntityQueryConfiguration<Order>{public void Configure(EntityQueryBuilder<Order> builder){//202105,202106...是默认的顺序,false表示使用反向排序,就是如果存在分片那么分片的tail将进行反向排序202202,202201,202112,202111....builder.ShardingTailComparer(Comparer<string>.Default, false);//order by createTime asc的顺序和分片ShardingTailComparer一样那么就用true//但是目前ShardingTailComparer是倒序所以order by createTime asc需要和他一样必须要是倒序,倒序就是falsebuilder.AddOrder(o => o.CreateTime,false);//配置当不存在Order的时候如果我是FirstOrDefault那么将采用和ShardingTailComparer相反的排序执行因为是false//默认从最早的表开始查询builder.AddDefaultSequenceQueryTrip(false, CircuitBreakerMethodNameEnum.FirstOrDefault);默认从最近表开始查询//builder.AddDefaultSequenceQueryTrip(true, CircuitBreakerMethodNameEnum.FirstOrDefault);//内部配置单表查询的FirstOrDefault connections limit限制为1builder.AddConnectionsLimit(1, LimitMethodNameEnum.FirstOrDefault);}}public class OrderRoute:AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute<Order>{//......//配置路由才用这个对象查询public override IEntityQueryConfiguration<Order> CreateEntityQueryConfiguration(){return new OrderQueryConfiguration();}}
带配置的Order
现在我们将默认的配置修改回正确
//不合适因为一般而言我们肯定是查询最新的所以应该和ShardingComparer一样都是倒序查询
//builder.AddDefaultSequenceQueryTrip(false, CircuitBreakerMethodNameEnum.FirstOrDefault);
builder.AddDefaultSequenceQueryTrip(true, CircuitBreakerMethodNameEnum.FirstOrDefault);
当然如果你希望本次查询不使用配置的连接数限制可以进行如下操作
_shardingDbContext.Set<Order>().UseConnectionMode(2).Where(o=>o.Id=="7").FirstOrDefaultAsync();
结论:当我们配置了默认分片表应该以何种顺序进行分片聚合时,如果相应的查询方法也进行了配置那么将这种查询视为顺序查询,
所有的顺序查询都符合上述表格模式,遇到对应的将直接进行熔断,不在进行后续的处理直接返回,保证高性能和防止无意义的查询。
快速失败FastFail
顾名思义就是快速失败,但是很多小伙伴可能不清楚这个快速失败的意思,失败就是失败了为什么有快速失败一说,因为ShardingCore内部的本质是将一个sql语句进行才分N条然后并行执行
-- 普通sqlselect * from order where id='1' or id='2'-- 分片sql
select * from order_1 where id='1' or id='2'
select * from order_2 where id='1' or id='2'
-- 分别对这两个sql进行并行执行
在正常情况下程序是没有什么问题的,但是由于程序是并行查询后迭代聚合所以会带来一个问题,就是假设执行order_1的线程挂掉了,那么Task.WhenAll会一致等待所有线程完成,然后抛出响应的错误,
那么这在很多情况下等于其余线程都在多无意义的操作,各自管各自。
static async Task Main(string[] args){try{await Task.WhenAll(DoSomething1(), DoSomething2());Console.WriteLine("execute success");}catch {Console.WriteLine("error");}Console.ReadLine();}static async Task<int> DoSomething1(){for (int i = 0; i < 10; i++){if (i == 2)throw new Exception("111");await Task.Delay(1000);Console.WriteLine("DoSomething1"+i);}return 1;}static async Task<int> DoSomething2(){for (int i = 0; i < 10; i++){await Task.Delay(1000);Console.WriteLine("DoSomething2"+i);}return 1;}
代码很简单就是Task.WhenAll
的时候执行两个委托方法,然后让其中一个快速抛异常的情况下看看是否马上返回
结果是TaskWhenAll
哪怕出现异常也需要等待所有的线程完成任务,这会在某些情况下浪费不必要的性能,所以这边ShardingCore
参考资料采用了FastFail
版本的
public static Task WhenAllFailFast(params Task[] tasks){if (tasks is null || tasks.Length == 0) return Task.CompletedTask;// defensive copy.var defensive = tasks.Clone() as Task[];var tcs = new TaskCompletionSource();var remaining = defensive.Length;Action<Task> check = t =>{switch (t.Status){case TaskStatus.Faulted:// we 'try' as some other task may beat us to the punch.tcs.TrySetException(t.Exception.InnerException);break;case TaskStatus.Canceled:// we 'try' as some other task may beat us to the punch.tcs.TrySetCanceled();break;default:// we can safely set here as no other task remains to run.if (Interlocked.Decrement(ref remaining) == 0){// get the results into an array.tcs.SetResult();}break;}};foreach (var task in defensive){task.ContinueWith(check, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);}return tcs.Task;}
采用failfast后当前主线程会直接在错误时返回,其余线程还是继续执行,需要自行进行canceltoken.cancel或者通过共享变量来取消执行
总结
ShardngCore
目前还在不断努力成长中,也希望各位多多包涵可以在使用中多多提出响应的意见和建议
demo https://github.com/xuejmnet/ShardingCircuitBreaker
参考资料
https://stackoverflow.com/questions/57313252/how-can-i-await-an-array-of-tasks-and-stop-waiting-on-first-exception
下期预告
下一篇我们将讲解如何让流式聚合支持更多的sql查询,如何将不支持的sql降级为union all
分库分表下极致的优化相关推荐
- 分库分表下分页查询解决方案
分库分表下分页查询解决方案 不管是随着业务量的增大.还是随着用户数量的增长,在单一表中无法承受大量大数据,导致查询速度极慢甚至拖垮数据库.所以分库分表的策略随之应用,但是如何在分库分表的情况下,进行分 ...
- Linux数据库管理——day10——分库分表、数据库硬件优化
分库分表 分库分表也称作分片技术,主要作用是将存放在一个数据库中的数据按照特定的方法进行拆分,分散存放在多个数据库中,以达到分散多台设备实现负载均衡 垂直分割 纵向切分,把一个表 ...
- 2天,我把MySQL索引、事务、分库分表、锁、性能优化撸完了!
Java研发工程师必备技能非MySQL莫属,虽说易学好上手,但应对大厂面试,最容易遭遇滑铁卢.功败垂成的也是它. 上手简单,玩转难,才是这款开源数据库叱咤业界多年的真实写照. MySQL 8.0正式版 ...
- 数据库 分库 分表 分区
我们知道,如果我们使用mysql,当数据库数据量达到一定数据量之后,会考虑对数据库进行分库分表等操作,但是在什么情况下做怎么的切分,下面分表介绍. 一.分库 1 分库原因 首先,在单台数据库服务器性能 ...
- mysql中的分库分表
大表怎么优化?某个表有近千万数据,CRUD比较慢,如何优化?分库分表了是怎么做的?分表分库了有什么问题?有用到中间件么?他们的原理知道么? 文章目录 大表怎么优化 分库分表 垂直分区 垂直分表 水平分 ...
- 分库分表实战(第1期):一叶知秋 —— 图览分库分表外卖订单项目
+VX:ruyuanhadeng获得600+页原创精品文章汇总PDF 前 言 各位读者朋友,大家好,这是分库分表实战的第一篇文章,首先介绍一下 "基于ShardingSphere的分库分表实 ...
- 数据库——MySQL分库分表的演进和实践以及中间件的比较
1.了解几个问题? 1.分库分表相关术语 读写分离: 不同的数据库,同步相同的数据,分别只负责数据的读和写: 分区: 指定分区列表达式,把记录拆分到不同的区域中(必须是同一服务器,可以是不同硬盘),应 ...
- 分库分表Sharding-JDBC最佳实践专题
一 Mysql数据库架构演变历史 单机 请求量大查询慢 单机故障导致业务不可用 主从 数据库主从同步,从库可以水平扩展,满足更大读需求 但单服务器TPS,内存,IO都是有限的 双主 用户量级上来后,写 ...
- 高并发系列:存储优化之也许可能是史上最详尽的分库分表文章之一
趣味性不强,但知识性很强,建议耐心看或者先收藏 本文内容预览: 库表会在哪天到达瓶颈? 1.1 苏宁拼购百万级库表拆分之前 1.2 京东配运平台库表拆分之前 1.3 大众点评订单库拆分之前 1.4 小 ...
最新文章
- 印度太阳能企业争取对中、台、马实施反倾销税
- 把在win7系统下,把笔记本的无线网卡变成路由器,共享上网。
- openjdk需要自己添加cacerts,
- 检查字符串是否包含数字
- 服务器打开虚拟机电源重启,虚拟机服务器自动重启
- Zipkin-1.19.0学习系列1:java范例
- CF1621G Weighted Increasing Subsequences(离散化+树状数组优化dp+栈维护后缀最大值+计数)
- JUnit和Mockito合作
- 在mount里看到哪个设备文件的一些笔记
- 职业生涯设计的10点忠告
- sublime中的emmet插件的使用技巧
- WIN2003 IIS6.0+PHP+ASP+MYSQL优化配置
- “酸碱体质理论”是个骗局
- Dynamips GNS3
- w3school学习笔记2(MySQL)
- 中国超级稻在18个亚非国家试种推广 国稻种芯百团计划行动
- 矩阵快速幂分析+POJ3070
- verilog实验1:基于FPGA蜂鸣器演奏乐曲并数码管显示
- Borel probability measure space (Borel 概率测度空间)是个啥?
- SIMT和SIMD之总结篇