您好,登錄后才能下訂單哦!
小編給大家分享一下.Net如何使用分表分庫框架ShardingCore實現多字段分片,希望大家閱讀完這篇文章之后都有所收獲,下面讓我們一起去探討吧!
我們現在假定一個很簡單的場景,依然是訂單時間按月分片,查詢進行如下語句
//這邊演示不使用雪花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.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,并且設置了一下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; } //當前時間的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的判斷而不需要全部情況都考慮
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;
直接讓所有的交集都是空所以不會進行一次無意義的數據庫查詢
看完了這篇文章,相信你對“.Net如何使用分表分庫框架ShardingCore實現多字段分片”有了一定的了解,如果想了解更多相關知識,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。