日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > asp.net >内容正文

asp.net

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

發布時間:2023/12/4 asp.net 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 .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

創建一個訂單對象

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來進行分片的那么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--------------");//訂單號和創建時間查詢 精確定位 無路由結果 拋錯或者返回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;//配置默認不拋出異常})

我們再次來看下測試結果

為何我們測試是不經過數據庫直接查詢,原因就是在我們做各個屬性分片交集的時候返回了空那么框架會選擇拋出異?;蛘叻祷啬J值兩種選項,并且我們在編寫路由的時候判斷格式不正確返回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下你不得不看的分表分库解决方案-多字段分片的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。