.Net使用分表分庫(kù)框架ShardingCore實(shí)現(xiàn)多字段分片
介紹
本期主角:ShardingCore 一款ef-core下高性能、輕量級(jí)針對(duì)分表分庫(kù)讀寫分離的解決方案,具有零依賴、零學(xué)習(xí)成本、零業(yè)務(wù)代碼入侵
dotnet下唯一一款全自動(dòng)分表,多字段分表框架,擁有高性能,零依賴、零學(xué)習(xí)成本、零業(yè)務(wù)代碼入侵,并且支持讀寫分離動(dòng)態(tài)分表分庫(kù),同一種路由可以完全自定義的新星組件,通過(guò)本框架你不但可以學(xué)到很多分片的思想和技巧,并且更能學(xué)到Expression的奇思妙用
你的star和點(diǎn)贊是我堅(jiān)持下去的最大動(dòng)力,一起為.net生態(tài)提供更好的解決方案
項(xiàng)目地址
github地址 https://github.com/xuejmnet/sharding-core
gitee地址 https://gitee.com/dotnetchina/sharding-core
背景
直接開(kāi)門見(jiàn)山,你有沒(méi)有這種情況你需要將一批數(shù)據(jù)用時(shí)間分片來(lái)進(jìn)行存儲(chǔ)比如訂單表,訂單表的分片字段是訂單的創(chuàng)建時(shí)間,并且id是雪花id,訂單編號(hào)是帶時(shí)間信息的編號(hào),因?yàn)?net下的所有分片方案幾乎都是只支持單分片字段,所以當(dāng)我們不使用分片字段查詢也就是訂單創(chuàng)建時(shí)間查詢的話會(huì)帶來(lái)全表查詢,導(dǎo)致性能下降,譬如我想用雪花id或者訂單編號(hào)進(jìn)行查詢,但是帶來(lái)的卻是內(nèi)部低效的結(jié)果,針對(duì)這種情況是否有一個(gè)好的解決方案呢,有但是需要侵入業(yè)務(wù)代碼,根據(jù)雪花id或者訂單編號(hào)進(jìn)行解析出對(duì)應(yīng)的時(shí)間然后手動(dòng)指定分片前提是框架支持手動(dòng)指定.基于上述原因ShardingCore 帶來(lái)了全新版本 x.3.2.x+ 支持多字段分片路由,并且擁有很完美的實(shí)現(xiàn),廢話不多說(shuō)我們直接開(kāi)始吧?。。。。。。。。。?!
原理
我們現(xiàn)在假定一個(gè)很簡(jiǎn)單的場(chǎng)景,依然是訂單時(shí)間按月分片,查詢進(jìn)行如下語(yǔ)句
//這邊演示不使用雪花id因?yàn)檠┗╥d很難在演示中展示所以使用訂單編號(hào)進(jìn)行演示格式: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();
上述語(yǔ)句OrderNo會(huì)查詢Order_202112這張表,然后時(shí)間索引會(huì)查詢......Order_202108、Order_202109、Order_202110,然后兩者取一個(gè)交集我們發(fā)現(xiàn)其實(shí)是沒(méi)有結(jié)果的,這個(gè)時(shí)候應(yīng)該是返回默認(rèn)值null或者直接報(bào)錯(cuò)
這就是一個(gè)簡(jiǎn)單的原理
直接開(kāi)始
接下來(lái)我將用訂單編號(hào)和創(chuàng)建時(shí)間來(lái)為大演示,數(shù)據(jù)庫(kù)采用sqlserver(你也可以換成任意efcore支持的數(shù)據(jù)庫(kù)),其中編號(hào)格式y(tǒng)yyyMMddHHmmss+new Random().Next(0,10000).ToString().PadLeft(4,'0'),創(chuàng)建時(shí)間是DateTime格式并且創(chuàng)建時(shí)間按月分表,這邊不采用雪花id是因?yàn)檠┗╥d的實(shí)現(xiàn)會(huì)根據(jù)workid和centerid的不一樣而出現(xiàn)不一樣的效果,接下來(lái)我們通過(guò)簡(jiǎn)單的5步操作實(shí)現(xiàn)多字段分片
添加依賴
首先我們添加兩個(gè)依賴,一個(gè)是ShardingCore一個(gè)EFCore.SqlServer
//請(qǐng)安裝最新版本目前x.3.2.x+,第一個(gè)版本號(hào)6代表efcore的版本號(hào) Install-Package ShardingCore -Version 6.3.2 Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 6.0.1


創(chuàng)建一個(gè)訂單對(duì)象
public class Order
{
public string Id { get; set; }
public string OrderNo { get; set; }
public string Name { get; set; }
public DateTime CreateTime { get; set; }
}
創(chuàng)建DbContext
這邊就簡(jiǎn)單的創(chuàng)建了一個(gè)dbcontext,并且設(shè)置了一下order如何映射到數(shù)據(jù)庫(kù),當(dāng)然你可以采用attribute的方式而不是一定要fluentapi
/// <summary>
/// 如果需要支持分表必須要實(shí)現(xiàn)<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; }
}
創(chuàng)建分片路由
這邊我們采用訂單創(chuàng)建時(shí)間按月分表
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>
/// 是否要在程序運(yùn)行期間自動(dòng)創(chuàng)建每月的表
/// </summary>
/// <returns></returns>
public override bool AutoCreateTableByTime()
{
return true;
}
/// <summary>
/// 分表從何時(shí)起創(chuàng)建
/// </summary>
/// <returns></returns>
public override DateTime GetBeginTime()
{
return new DateTime(2021, 9, 1);
}
/// <summary>
/// 配置額外分片路由規(guī)則
/// </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>
/// 訂單編號(hào)的路由
/// </summary>
/// <param name="shardingKey"></param>
/// <param name="shardingOperator"></param>
/// <returns></returns>
private Expression<Func<string, bool>> GetOrderNoRouteFilter(object shardingKey,
ShardingOperatorEnum shardingOperator)
{
//將分表字段轉(zhuǎn)成訂單編號(hào)
var orderNo = shardingKey?.ToString() ?? string.Empty;
//判斷訂單編號(hào)是否是我們符合的格式
if (!CheckOrderNo(orderNo, out var orderTime))
{
//如果格式不一樣就直接返回false那么本次查詢因?yàn)槭莂nd鏈接的所以本次查詢不會(huì)經(jīng)過(guò)任何路由,可以有效的防止惡意攻擊
return tail => false;
}
//當(dāng)前時(shí)間的tail
var currentTail = TimeFormatToTail(orderTime);
//因?yàn)槭前丛路直硭垣@取下個(gè)月的時(shí)間判斷id是否是在臨界點(diǎn)創(chuàng)建的
var nextMonthFirstDay = ShardingCoreHelper.GetNextMonthFirstDay(DateTime.Now);
if (orderTime.AddSeconds(10) > nextMonthFirstDay)
{
var nextTail = TimeFormatToTail(nextMonthFirstDay);
return DoOrderNoFilter(shardingOperator, orderTime, currentTail, nextTail);
}
//因?yàn)槭前丛路直硭垣@取這個(gè)月月初的時(shí)間判斷id是否是在臨界點(diǎn)創(chuàng)建的
if (orderTime.AddSeconds(-10) < ShardingCoreHelper.GetCurrentMonthFirstDay(DateTime.Now))
{
//上個(gè)月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不應(yīng)該被返回
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;
}
}
這邊我來(lái)講解一下為什么用額外字段分片需要些這么多代碼呢,其實(shí)是這樣的因?yàn)槟闶怯糜唵蝿?chuàng)建時(shí)間CreateTime來(lái)進(jìn)行分片的那么CreateTime和OrderNo的賦值原理上說(shuō)應(yīng)該在系統(tǒng)里面是不可能實(shí)現(xiàn)同一時(shí)間賦值的肯定有先后關(guān)系可能是幾微妙甚至幾飛秒,但是為了消除這種差異這邊采用了臨界點(diǎn)兼容算法來(lái)實(shí)現(xiàn),讓我們來(lái)看下一下代碼
var order=new Order()
//執(zhí)行這邊生成出來(lái)的id是2021-11-30 23:59:59.999.999
order.OrderNo=DateTime.Now.ToString("yyyyMMddHHmmss")+"xxx";
//business code //具體執(zhí)行時(shí)間不確定,哪怕沒(méi)有business code也沒(méi)有辦法保證兩者生成的時(shí)間一致,當(dāng)然如果你可以做到一致完全不需要這么復(fù)雜的編寫
............
//執(zhí)行這邊生成出來(lái)的時(shí)間是2021-12-01 00:00:00.000.000
order.CreateTime=DateTime.Now;
當(dāng)然系統(tǒng)里面采用了前后添加10秒是一個(gè)比較保守的估算你可以采用前后一秒甚至幾百毫秒都是ok的,具體業(yè)務(wù)具體實(shí)現(xiàn),因?yàn)榇蟛糠值膭?chuàng)建時(shí)間可能是由框架在提交后才會(huì)生成而不是new Order的時(shí)候,當(dāng)然也不排除這種情況,當(dāng)然如果你只需要考慮equal一種情況可以只編寫equal的判斷而不需要全部情況都考慮
ShardingCore啟動(dòng)配置
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啟動(dòng)時(shí)有哪些表
.End();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.Services.GetRequiredService<IShardingBootstrapper>().Start();
app.UseAuthorization();
app.MapControllers();
//額外添加一些種子數(shù)據(jù)
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();
整個(gè)配置下來(lái)其實(shí)也就兩個(gè)地方需要配置還是相對(duì)比較簡(jiǎn)單的,直接啟動(dòng)開(kāi)始我們的測(cè)試模式
測(cè)試
默認(rèn)配置下的測(cè)試
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--------------");
//訂單編號(hào)查詢 精確定位
Console.WriteLine("--------------Query OrderNo Begin--------------");
var order2 = await _defaultDbContext.Set<Order>().Where(o=>o.OrderNo== "202110080000000003").FirstOrDefaultAsync();
Console.WriteLine("--------------Query OrderNo End--------------");
//創(chuàng)建時(shí)間查詢 精確定位
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--------------");
//訂單編號(hào)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--------------");
//訂單號(hào)和創(chuàng)建時(shí)間查詢 精確定位 無(wú)路由結(jié)果 拋錯(cuò)或者返回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--------------");
//非正確格式訂單號(hào) 拋錯(cuò)或者返回default防止擊穿數(shù)據(jù)庫(kù)
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();
}
測(cè)試結(jié)果

測(cè)試結(jié)果非常完美除了無(wú)法匹配路由的時(shí)候那么我們?cè)撊绾卧O(shè)置呢
測(cè)試無(wú)路由返回默認(rèn)值
builder.Services.AddShardingDbContext<DefaultDbContext>(...)
.Begin(o =>
{
....
o.ThrowIfQueryRouteNotMatch = false;//配置默認(rèn)不拋出異常
})
我們?cè)俅蝸?lái)看下測(cè)試結(jié)果

為何我們測(cè)試是不經(jīng)過(guò)數(shù)據(jù)庫(kù)直接查詢,原因就是在我們做各個(gè)屬性分片交集的時(shí)候返回了空那么框架會(huì)選擇拋出異?;蛘叻祷啬J(rèn)值兩種選項(xiàng),并且我們?cè)诰帉懧酚傻臅r(shí)候判斷格式不正確返回 return tail => false;直接讓所有的交集都是空所以不會(huì)進(jìn)行一次無(wú)意義的數(shù)據(jù)庫(kù)查詢
總結(jié)
看到這邊你應(yīng)該已經(jīng)看到了本框架的強(qiáng)大之處,本框架不但可以實(shí)現(xiàn)多字段分片還可以實(shí)現(xiàn)自定義分片,而不是單單按時(shí)間分片這么簡(jiǎn)單,我完全可以設(shè)置訂單從2021年后的訂單按月分片,2021年前的訂單按年分片,對(duì)于sharding-core而言這簡(jiǎn)直輕而易舉,但是據(jù)我所知.Net下目前除了我沒(méi)有任何一款框架可以做到真正的全自動(dòng)分片+多字段分片,所以我們?cè)谠O(shè)計(jì)框架分片的時(shí)候盡可能的將有用的信息添加到一些無(wú)意義的字段上比如Id可以有效的解決很多在大數(shù)據(jù)下發(fā)生的問(wèn)題,你可以簡(jiǎn)單理解為我加了一個(gè)索引并且附帶了額外列,我加了一個(gè)id并且?guī)Я朔直硇畔⒃诶锩?也可以完全設(shè)計(jì)出一款附帶分庫(kù)的屬性到id里面使其可以支持分表分庫(kù)
demo地址 https://github.com/xuejmnet/MultiShardingProperties
到此這篇關(guān)于.Net使用分表分庫(kù)框架ShardingCore實(shí)現(xiàn)多字段分片的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
asp.net Execl的添加,更新操作實(shí)現(xiàn)代碼
asp.net Execl的添加、修改等實(shí)現(xiàn)代碼。2009-02-02
如何利用擴(kuò)展方法來(lái)鏈?zhǔn)降膶?duì)MVC 3中的頁(yè)面進(jìn)行驗(yàn)證
雖然擴(kuò)展方法只是改變了我們寫代碼的方式,但是如果我們使用得當(dāng),可以給我們帶來(lái)巨大的編碼效率的提升接下來(lái)介紹通過(guò)擴(kuò)展方法(鏈?zhǔn)椒椒ǎ镸VC 3視圖添加驗(yàn)證2013-01-01
asp.net讀取excel中的數(shù)據(jù)并綁定在gridview
這篇文章主要介紹了asp.net讀取excel中的數(shù)據(jù)并綁定在gridview上的方法,需要的朋友可以參考下2014-02-02
asp.net 動(dòng)態(tài)添加多個(gè)用戶控件
動(dòng)態(tài)添加多個(gè)相同用戶控件,并使每個(gè)用戶控件獲取不同的內(nèi)容。2009-12-12
ASP.NET MVC DropDownList數(shù)據(jù)綁定及使用詳解
DropDownList 控件用于創(chuàng)建下拉列表。DropDownList 控件中的每個(gè)可選項(xiàng)都是由 ListItem 元素定義的!該控件支持?jǐn)?shù)據(jù)綁定2012-12-12
ASP.NET Core3.X 終端中間件轉(zhuǎn)換為端點(diǎn)路由運(yùn)行詳解
這篇文章主要介紹了ASP.NET Core3.X 終端中間件轉(zhuǎn)換為端點(diǎn)路由運(yùn)行,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
概述.net開(kāi)發(fā)過(guò)程中Bin目錄下面幾種文件格式
本篇文章主要對(duì)項(xiàng)目發(fā)布的時(shí)候,經(jīng)常用到的幾個(gè)文件:.pdb、.xsd、.vshost.exe、.exe、.exe.config、.vshost.exe.config的作用進(jìn)行介紹,具有一定的參考價(jià)值,需要的朋友可以看下2016-12-12

