分库分表下极致的优化
題外話
這邊說一句題外話,就是ShardingCore目前已經(jīng)正式加入?NCC?開源組織了,也是希望框架和社區(qū)能發(fā)展的越來越好,希望為更多.netter提供解決方案和開源組件
介紹
依照慣例首先介紹本期主角:ShardingCore?一款ef-core下高性能、輕量級針對分表分庫讀寫分離的解決方案,具有零依賴、零學(xué)習(xí)成本、零業(yè)務(wù)代碼入侵
dotnet下唯一一款全自動分表,多字段分表框架,擁有高性能,零依賴、零學(xué)習(xí)成本、零業(yè)務(wù)代碼入侵,并且支持讀寫分離動態(tài)分表分庫,同一種路由可以完全自定義的新星組件框架
你的star和點贊是我堅持下去的最大動力,一起為.net生態(tài)提供更好的解決方案
項目地址
github地址?https://github.com/dotnetcore/sharding-core
gitee地址?https://gitee.com/dotnetchina/sharding-core
本次優(yōu)化點
直奔主題來講下本次的極致優(yōu)化具體是優(yōu)化了什么,簡單說就是CircuitBreaker和FastFail.
斷路器CircuitBreaker
我們假設(shè)這么一個場景,現(xiàn)在我們有一個訂單order表,訂單會按照月份進行分片,那么訂單表會有如下幾個order_202201、order_202202、order_202203、order_202204、order_202205,假設(shè)我們有5張表。
首先我們來看一條普通的語句
這是一條普通的不能在普通的sql了,查詢第一條id是xxx的訂單,
那么他在分表下面會如何運行
這個操作我相信很多同學(xué)都是可以了解的,稍微熟悉點分表分庫的同學(xué)應(yīng)該都知道這是基本操作了,但是這個操作看似高效(時間上)但是在連接數(shù)上而言并不是那么的高效,因為同一時間需要開打的連接數(shù)將由5個
那么在這個背景下ShardingCore參考ShardingSphere?提供了更加友好的連接控制和內(nèi)存聚合模式ConnectionMode
這個張圖上我們可以清晰的看到不同的數(shù)據(jù)庫直接才用了一個并發(fā)限制,比如設(shè)置的是2,那么在相同庫里面的查詢將是每2個一組,進行查詢,這樣可以控制在同一個數(shù)據(jù)庫下的連接數(shù),進而解決了客戶端連接模式下的連接數(shù)消耗猛烈的一個弊端。
//開啟5個線程并發(fā)查詢 {//并行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 } //查詢出來的結(jié)果在內(nèi)存中進行聚合成一個list集合 //然后在對這個list集合進行第一條的獲取 list.Where(o=>o is not null).FirstOrDefault()到目前為止這邊已經(jīng)對分片的查詢優(yōu)化到了一個新的高度。但是雖然我們優(yōu)化了連接數(shù)的處理,但是就查詢速度而言基本上是沒有之前的那么快,可以說和你分組的組數(shù)成線性增加時間的消耗。
所以到此為止ShardingCore又再一次進化出了全新的翅膀CircuitBreaker斷路器,我們繼續(xù)往下看
我們現(xiàn)在的sql是
select * from order where id='xxx' limit 1那么如果我們針對這個sql進行優(yōu)化呢,譬如
select * from order where id='xxx' order by create_time desc limit 1同樣是查詢第一條,添加了一個order排序那么情況就會大大的不一樣,首先我們來觀察我們的分片查詢
//開啟5個線程并發(fā)查詢 -- 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個一組,那么我們在查詢第一組的結(jié)果后是否就可以直接拋棄掉下面的所有查詢,也就是我們只需要查詢
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只要他們是有返回一個以上的數(shù)據(jù)那么本次分片查詢將會被終止,ShardingCore目前的大殺器,本來年前已經(jīng)開發(fā)完成了,奈何太懶只是發(fā)布了版本并沒有相關(guān)的說明和使用方法
CircuitBreaker
斷路器,它具有類似拉閘中斷操作的功能,這邊簡單說下linq操作下的部分方法的斷路器點在哪里
| First | 支持 | 按順序查詢到第一個時就可以放棄其余查詢 |
| FirstOrDefault | 支持 | 按順序查詢到第一個時就可以放棄其余查詢 |
| Last | 支持 | 按順序倒敘查詢到第一個時就可以放棄其余查詢 |
| LastOrDefault | 支持 | 按順序倒敘查詢到第一個時就可以放棄其余查詢 |
| Single | 支持 | 查詢到兩個時就可以放棄,因為元素個數(shù)大于1個了需要拋錯 |
| SingleOrDefault | 支持 | 查詢到兩個時就可以放棄,因為元素個數(shù)大于1個了需要拋錯 |
| Any | 支持 | 查詢一個結(jié)果true就可以放棄其余查詢 |
| All | 支持 | 查詢到一個結(jié)果fasle就可以放棄其余查詢 |
| Contains | 支持 | 查詢一個結(jié)果true就可以放棄其余查詢 |
| Count | 不支持 | -- |
| LongCount | 不支持 | -- |
| Max | 支持 | 按順序最后一條并且查詢最大字段是分片順序同字段是,max的屬性只需要查詢一條記錄 |
| Min | 支持 | 按順序第一條并且查詢最小字段是分片順序同字段,min的屬性只需要查詢一條記錄 |
| Average | 不支持 | -- |
| Sum | 不支持 | -- |
這邊其實只有三個操作是任何狀態(tài)下都可以支持中斷,其余操作需要在額外條件順序查詢的情況下才可以,并且我們本次查詢分片涉及到過多的后綴表那么性能和資源的利用將會大大提升
查詢配置
廢話不多說我們開始以mysql作為本次案例(不要問我為什么不用SqlServer,因為寫文章的時候我是mac電腦),這邊我們創(chuàng)建一個項目新建一個訂單按月分表
新建項目
安裝依賴
添加訂單表和訂單表映射
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月份開始按創(chuàng)建時間建表
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);}}啟動配置
簡單的配置啟動創(chuàng)建表和庫,并且添加種子數(shù)據(jù)
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();這邊默認(rèn)連接模式的分組是Environment.ProcessorCount
編寫查詢
沒有配置的情況下那么這個查詢將是十分糟糕
接下來我們將配置Order的查詢
public class OrderQueryConfiguration:IEntityQueryConfiguration<Order>{public void Configure(EntityQueryBuilder<Order> builder){//202105,202106...是默認(rèn)的順序,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);//配置當(dāng)不存在Order的時候如果我是FirstOrDefault那么將采用和ShardingTailComparer相反的排序執(zhí)行因為是false//默認(rèn)從最早的表開始查詢builder.AddDefaultSequenceQueryTrip(false, CircuitBreakerMethodNameEnum.FirstOrDefault);默認(rèn)從最近表開始查詢//builder.AddDefaultSequenceQueryTrip(true, CircuitBreakerMethodNameEnum.FirstOrDefault);//內(nèi)部配置單表查詢的FirstOrDefault connections limit限制為1builder.AddConnectionsLimit(1, LimitMethodNameEnum.FirstOrDefault);}}public class OrderRoute:AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute<Order>{//......//配置路由才用這個對象查詢public override IEntityQueryConfiguration<Order> CreateEntityQueryConfiguration(){return new OrderQueryConfiguration();}}
帶配置的Order
現(xiàn)在我們將默認(rèn)的配置修改回正確
//不合適因為一般而言我們肯定是查詢最新的所以應(yīng)該和ShardingComparer一樣都是倒序查詢 //builder.AddDefaultSequenceQueryTrip(false, CircuitBreakerMethodNameEnum.FirstOrDefault); builder.AddDefaultSequenceQueryTrip(true, CircuitBreakerMethodNameEnum.FirstOrDefault);
當(dāng)然如果你希望本次查詢不使用配置的連接數(shù)限制可以進行如下操作
結(jié)論:當(dāng)我們配置了默認(rèn)分片表應(yīng)該以何種順序進行分片聚合時,如果相應(yīng)的查詢方法也進行了配置那么將這種查詢視為順序查詢,
所有的順序查詢都符合上述表格模式,遇到對應(yīng)的將直接進行熔斷,不在進行后續(xù)的處理直接返回,保證高性能和防止無意義的查詢。
快速失敗FastFail
顧名思義就是快速失敗,但是很多小伙伴可能不清楚這個快速失敗的意思,失敗就是失敗了為什么有快速失敗一說,因為ShardingCore內(nèi)部的本質(zhì)是將一個sql語句進行才分N條然后并行執(zhí)行
-- 普通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進行并行執(zhí)行在正常情況下程序是沒有什么問題的,但是由于程序是并行查詢后迭代聚合所以會帶來一個問題,就是假設(shè)執(zhí)行order_1的線程掛掉了,那么Task.WhenAll會一致等待所有線程完成,然后拋出響應(yīng)的錯誤,
那么這在很多情況下等于其余線程都在多無意義的操作,各自管各自。
代碼很簡單就是Task.WhenAll的時候執(zhí)行兩個委托方法,然后讓其中一個快速拋異常的情況下看看是否馬上返回
結(jié)果是TaskWhenAll哪怕出現(xiàn)異常也需要等待所有的線程完成任務(wù),這會在某些情況下浪費不必要的性能,所以這邊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后當(dāng)前主線程會直接在錯誤時返回,其余線程還是繼續(xù)執(zhí)行,需要自行進行canceltoken.cancel或者通過共享變量來取消執(zhí)行
總結(jié)
ShardngCore目前還在不斷努力成長中,也希望各位多多包涵可以在使用中多多提出響應(yīng)的意見和建議
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
下期預(yù)告
下一篇我們將講解如何讓流式聚合支持更多的sql查詢,如何將不支持的sql降級為union all
總結(jié)
以上是生活随笔為你收集整理的分库分表下极致的优化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET6之MiniAPI(十六):数据
- 下一篇: 使用Brighter实现轻量型独立管道