.NET 分庫分表高性能:瀑布流分頁
框架介紹
依照慣例首先介紹本期主角:ShardingCore 一款ef-core下高性能、輕量級針對分表分庫讀寫分離的解決方案,具有零依賴、零學習成本、零業(yè)務代碼入侵
dotnet下唯一一款全自動分表,多字段分表框架,擁有高性能,零依賴、零學習成本、零業(yè)務代碼入侵,并且支持讀寫分離動態(tài)分表分庫,同一種路由可以完全自定義的新星組件框架。
項目地址
github:https://github.com/dotnetcore/sharding-core
gitee:https://gitee.com/dotnetchina/sharding-core
背景
在大數據量下針對app端的瀑布流頁面分頁的優(yōu)化實戰(zhàn),有大量的數據,前端需要以瀑布流的形式展示出來,我們最簡單的就是以用戶發(fā)布的文章為例,假設我們有大量的文章帖子被,需求需要按帖子的發(fā)布時間倒序展示給用戶看,那么在手機端我們一般都是以下拉刷新,上拉加載的形式去展示,那么我們一般會有以下集中寫法。
常規(guī)分頁操作
select?count(*)?from?article
select?*?from?article?order?by?publish_time?desc?limit?0,20
這個操作是一般我們的常規(guī)分頁操作,先進行total然后進行分頁獲取,這種做法的好處是支持任意規(guī)則的分頁,缺點就是需要查詢兩次,一次count一次limit當然后期數據量實在太大可以只需要第一次count,但是也有一個問題就是如果數據量一直在變化會出現下一次分頁中還會有上一次的部分數據,因為數據在不斷地新增,你的分頁沒跟上發(fā)布的速度那么就會有這個情況發(fā)送.
瀑布流分頁
除了上述常規(guī)分頁操作外,我們針對特定順序的分頁也可以進行特定的分頁方式來實現高性能,因為基于大前提我們是大數量下的瀑布流,我們的文章假設是以雪花id作為主鍵,那么我們的分頁可以這么寫
select?*?from?article?where?idorder?by?publish_time?desc?limit?0,20
首先我們來分析一下,這個語句是利用了插入的數據分布是順序和你需要查詢的排序一直來實現的,又因為id不會重復并且雪花id的順序和時間是一致的都是同向的所以可以利用這種方式來進行排序,limit每次不需要跳過任何數目,直接獲取需要的數目即可,只需要傳遞上一次的查詢結果的id即可,這個方式彌補了上述常規(guī)分頁帶來的問題,并且擁有非常高的性能,但是缺點也顯而易見,不支持跳頁,不支持任意排序,所以這個方式目前來說非常適合前端app的瀑布流排序。
分片下的實現
首先分片下需要實現這個功能我們需要有id支持分片,并且publish_time按時間分表,兩者缺一不可。
原理
假設文章表article我們是以publish_time作為分片字段,假設按天分表,那么我們會擁有如下的表
article_20220101、article_20220102、article_20220103、article_20220104、article_20220105、article_20220106......
雪花id輔助分片
因為雪花id可以反解析出時間,所以我們對雪花id的=,>=,>,<=,<,contains的操作都是可以進行輔助分片進行縮小分片范圍
假設我們的雪花id解析出來是2021-01-05 11:11:11,那么針對這個雪花id的<小于操作我們可以等價于x < 2021-01-05 11:11:11,那么如果我問你這下我們需要查詢的表有哪些,很明顯 [article_20220101、article_20220102、article_20220103、article_20220104、article_20220105],除了20220106外我們都需要查詢。
union all分片模式
如果你使用union all的分片模式那么通常會將20220101-20220105的所有的表進行union all然后機械能過濾,那么優(yōu)點可想而知:簡單,連接數消耗僅1個,sql語句支持的多,缺點也顯而易見,優(yōu)化起來后期是個很大的問題,并且跨庫下的使用有問題
select?*?from?
(select?*?from?article_20220101?union?all?select?*?from?article_20220102?union?all?select?*?from?article_20220103....)?t
?where?idorder?by?publish_time?desc?limit?0,20
流式分片,順序查詢
如果你是流式分片模式進行聚合通常我們會將20220101-20220105的所有的表進行并行的分別查詢,然后針對每個查詢的結果集進行優(yōu)先級隊列的排序后獲取,優(yōu)點:語句簡單便于優(yōu)化,性能可控,支持分庫,缺點:實現復雜,連接數消耗多
select?*?from?article_20220101?where?idorder?by?publish_time?desc?limit?0,20
select?*?from?article_20220102where?idorder?by?publish_time?desc?limit?0,20
select?*?from?article_20220103?where?idorder?by?publish_time?desc?limit?0,20
......
流式分片下的優(yōu)化
目前 ShardingCore采用的是流式聚合+union all,當且僅當用戶手動3調用UseUnionAllMerge時會將分片sql轉成union all 聚合。
針對上述瀑布流的分頁ShardingCore是這么操作的
確定分片表的順序,也就是因為分片字段是 publish_time,又因為排序字段是publish_time所以分片表其實是有順序的,也就是[article_20220105、article_20220104、article_20220103、article_20220102、article_20220101], 因為我們是開啟n個并發(fā)線程所以這個排序可能沒有意義,但是如果我們是僅開啟設置單個連接并發(fā)的時候,程序將現在通過id進行表篩選,之后依次從大到小進行獲取直到滿足skip+take也就是0+20=20條數據后,進行直接拋棄剩余查詢返回結果,那么本次查詢基本上就是和單表查詢一樣,因為基本上最多跨兩張表基本可以滿足要求(具體場景不一定) 說明:假設 last_id反解析出來的結果是2022-01-04 05:05:05那么可以基本上排除article_20220105,判斷并發(fā)連接數如果是1那么直接查詢article_20220104,如果不滿足繼續(xù)查詢article_20220103,直到查詢結果為20條如果并發(fā)連接數是2那么查詢[article_20220104、article_20220103]如果不滿足繼續(xù)下面兩張表直到獲取到結果為20條數據,所以我們可以很清晰的了解其工作原理并且來優(yōu)化
說明
通過上述優(yōu)化可以保證流式聚合查詢在順序查詢下的高性能O(1) 通過上述優(yōu)化可以保證客戶端分片擁有最小化連接數控制 設置合理的主鍵可以有效的解決我們在大數據分片下的性能優(yōu)化
實踐
ShardingCore目前針對分片查詢進行了不斷地優(yōu)化和盡可能的無業(yè)務代碼入侵來實現高性能分片查詢聚合。
接下來我將為大家展示一款dotnet下唯一一款全自動路由、多字段分片、無代碼入侵、高性能順序查詢的框架在傳統(tǒng)數據庫領域下的分片功能,如果你使用過我相信你一定會愛上他。
第一步 安裝依賴
#?ShardingCore核心框架?版本6.4.2.4+
PM>?Install-Package?ShardingCore
#?數據庫驅動這邊選擇的是mysql的社區(qū)驅動?efcore6最新版本即可
PM>?Install-Package?Pomelo.EntityFrameworkCore.MySql
第二步 添加對象和上下文
有很多朋友問我一定需要使用fluentapi來使用ShardingCore嗎,只是個人喜好,這邊我才用dbset+attribute來實現
//文章表
[Table(nameof(Article))]
public?class?Article
{
????[MaxLength(128)]
????[Key]
????public?string?Id?{?get;?set;?}
????[MaxLength(128)]
????[Required]
????public?string?Title?{?get;?set;?}
????[MaxLength(256)]
????[Required]
????public?string?Content?{?get;?set;?}
????
????public?DateTime?PublishTime?{?get;?set;?}
}
context
public?class?MyDbContext:AbstractShardingDbContext,IShardingTableDbContext
{
????public?MyDbContext(DbContextOptions?options )?:?base(options)
????{
添加會導致efcore?的model提前加載的方法如Database.xxxx
????}
????public?IRouteTail?RouteTail?{?get;?set;?}
????
????public?DbSet?Articles?{?get;?set;?}
}
第三步 添加文章路由
public?class?ArticleRoute:AbstractSimpleShardingDayKeyDateTimeVirtualTableRoute<Article>
{
????public?override?void?Configure(EntityMetadataTableBuilder?builder )
????{
????????builder.ShardingProperty(o?=>?o.PublishTime);
????}
????public?override?bool?AutoCreateTableByTime()
????{
????????return?true;
????}
????public?override?DateTime?GetBeginTime()
????{
????????return?new?DateTime(2022,?3,?1);
????}
}
到目前為止基本上Article已經支持了按天分表
第四步 添加查詢配置,讓框架知道我們是順序分表且定義分表的順序
public?class?TailDayReverseComparer?:?IComparer<string>
{
????public?int?Compare(string??x,?string??y)
????{
????????//程序默認使用的是正序也就是按時間正序排序我們需要使用倒序所以直接調用原生的比較器然后乘以負一即可
????????return?Comparer<string>.Default.Compare(x,?y)?*?-1;
????}
}
//當前查詢滿足的復核條件必須是單個分片對象的查詢,可以join普通非分片表
public?class?ArticleEntityQueryConfiguration:IEntityQueryConfiguration<Article>
{
????public?void?Configure(EntityQueryBuilder?builder )
????{
????????//設置默認的框架針對Article的排序順序,這邊設置的是倒序
????????builder.ShardingTailComparer(new?TailDayReverseComparer());
????????////如下設置和上述是一樣的效果讓框架真對Article的后綴排序使用倒序
????????//builder.ShardingTailComparer(Comparer.Default,?false);
????????
????????//簡單解釋一下下面這個配置的意思
????????//第一個參數表名Article的哪個屬性是順序排序和Tail按天排序是一樣的這邊使用了PublishTime
????????//第二個參數表示對屬性PublishTime?asc時是否和上述配置的ShardingTailComparer一致,true表示一致,很明顯這邊是相反的因為默認已經設置了tail排序是倒序
????????//第三個參數表示是否是Article屬性才可以,這邊設置的是名稱一樣也可以,因為考慮到匿名對象的select
????????builder.AddOrder(o?=>?o.PublishTime,?false,SeqOrderMatchEnum.Owner|SeqOrderMatchEnum.Named);
????????//這邊為了演示使用的id是簡單的時間格式化所以和時間的配置一樣
????????builder.AddOrder(o?=>?o.Id,?false,SeqOrderMatchEnum.Owner|SeqOrderMatchEnum.Named);
????????//這邊設置如果本次查詢默認沒有帶上述配置的order的時候才用何種排序手段
????????//第一個參數表示是否和ShardingTailComparer配置的一樣,目前配置的是倒序,也就是從最近時間開始查詢,如果是false就是從最早的時間開始查詢
????????//后面配置的是熔斷器,也就是復核熔斷條件的比如FirstOrDefault只需要滿足一個就可以熔斷
????????builder.AddDefaultSequenceQueryTrip(true,?CircuitBreakerMethodNameEnum.Enumerator,?CircuitBreakerMethodNameEnum.FirstOrDefault);
????????//這邊配置的是當使用順序查詢配置的時候默認開啟的連接數限制是多少,startup一開始可以設置一個默認是當前cpu的線程數,這邊優(yōu)化到只需要一個線程即可,當然如果跨表那么就是串行執(zhí)行
????????builder.AddConnectionsLimit(1,?LimitMethodNameEnum.Enumerator,?LimitMethodNameEnum.FirstOrDefault);
????}
}
第五步 添加配置到路由
public?class?ArticleRoute:AbstractSimpleShardingDayKeyDateTimeVirtualTableRoute<Article>
{
????//省略.....
????public?override?IEntityQueryConfiguration?CreateEntityQueryConfiguration()
????{
????????return?new?ArticleEntityQueryConfiguration();
????}
}
第六步 startup配置
var?builder?=?WebApplication.CreateBuilder(args);
//?Add?services?to?the?container.
ILoggerFactory?efLogger?=?LoggerFactory.Create(builder?=>
{
????builder.AddFilter((category,?level)?=>?category?==?DbLoggerCategory.Database.Command.Name?&&?level?==?LogLevel.Information).AddConsole();
});
builder.Services.AddControllers();
builder.Services.AddShardingDbContext()
????.AddEntityConfig(o?=>
????{
????????o.CreateShardingTableOnStart?=?true;
????????o.EnsureCreatedWithOutShardingTable?=?true;
????????o.AddShardingTableRoute();
????})
????.AddConfig(o?=>
????{
????????o.ConfigId?=?"c1";
????????o.UseShardingQuery((conStr,?b)?=>
????????{
????????????b.UseMySql(conStr,?new?MySqlServerVersion(new?Version())).UseLoggerFactory(efLogger);
????????});
????????o.UseShardingTransaction((conn,?b)?=>
????????{
????????????b.UseMySql(conn,?new?MySqlServerVersion(new?Version())).UseLoggerFactory(efLogger);
????????});
????????o.AddDefaultDataSource("ds0",?"server=127.0.0.1;port=3306;database=ShardingWaterfallDB;userid=root;password=root;");
????????o.ReplaceTableEnsureManager(sp?=>?new?MySqlTableEnsureManager());
????}).EnsureConfig();
var?app?=?builder.Build();
app.Services.GetRequiredService().Start();
using?(var?scope?=?app.Services.CreateScope())
{
????var?myDbContext?=?scope.ServiceProvider.GetRequiredService();
????if?(!myDbContext.Articles.Any())
????{
????????List?articles?=?new?List();
????????var?beginTime?=?new?DateTime(2022,?3,?1,?1,?1,1);
????????for?(int?i?=?0;?i?70;?i++)
????????{
????????????var?article?=?new?Article();
????????????article.Id?=?beginTime.ToString("yyyyMMddHHmmss");
????????????article.Title?=?"標題"?+?i;
????????????article.Content?=?"內容"?+?i;
????????????article.PublishTime?=?beginTime;
????????????articles.Add(article);
????????????beginTime=?beginTime.AddHours(2).AddMinutes(3).AddSeconds(4);
????????}
????????myDbContext.AddRange(articles);
????????myDbContext.SaveChanges();
????}
}
app.MapControllers();
app.Run();
第七步 編寫查詢表達式
public?async?Task?Waterfall([FromQuery]?string?lastId,[FromQuery]int?take)
{
????Console.WriteLine($"-----------開始查詢,lastId:[{lastId}],take:[{take}]-----------");
????var?list?=?await?_myDbContext.Articles.WhereIf(o?=>?String.Compare(o.Id,?lastId)?0,!string.IsNullOrWhiteSpace(lastId)).Take(take)..OrderByDescending(o?=>?o.PublishTime)ToListAsync();
????return?Ok(list);
}
運行程序
因為07表是沒有的所以這次查詢會查詢07和06表,之后我們進行下一次分頁傳入上次id

因為沒有對Article.Id進行分片路由的規(guī)則編寫所以沒辦法進行對id的過濾,那么接下來我們配置Id的分片規(guī)則
首先針對ArticleRoute進行代碼編寫
public?class?ArticleRoute:AbstractSimpleShardingDayKeyDateTimeVirtualTableRoute<Article>
{
????public?override?void?Configure(EntityMetadataTableBuilder?builder )
????{
????????builder.ShardingProperty(o?=>?o.PublishTime);
????????builder.ShardingExtraProperty(o?=>?o.Id);
????}
????public?override?bool?AutoCreateTableByTime()
????{
????????return?true;
????}
????public?override?DateTime?GetBeginTime()
????{
????????return?new?DateTime(2022,?3,?1);
????}
????public?override?IEntityQueryConfiguration?CreateEntityQueryConfiguration()
????{
????????return?new?ArticleEntityQueryConfiguration();
????}
????public?override?Expressionstring,?bool>>?GetExtraRouteFilter(object?shardingKey,?ShardingOperatorEnum?shardingOperator,?string?shardingPropertyName)
????{
????????switch?(shardingPropertyName)
????????{
????????????case?nameof(Article.Id):?return?GetArticleIdRouteFilter(shardingKey,?shardingOperator);
????????}
??????return?base.GetExtraRouteFilter(shardingKey,?shardingOperator,?shardingPropertyName);
????}
????///?
????///?文章id的路由
????///?
????///?
????///?
????///?
????private?Expressionstring,?bool>>?GetArticleIdRouteFilter(object?shardingKey,
????????ShardingOperatorEnum?shardingOperator)
????{
????????//將分表字段轉成訂單編號
????????var?id?=?shardingKey?.ToString()????string.Empty;
????????//判斷訂單編號是否是我們符合的格式
????????if?(!CheckArticleId(id,?out?var?orderTime))
????????{
????????????//如果格式不一樣就直接返回false那么本次查詢因為是and鏈接的所以本次查詢不會經過任何路由,可以有效的防止惡意攻擊
????????????return?tail?=>?false;
????????}
????????//當前時間的tail
????????var?currentTail?=?TimeFormatToTail(orderTime);
????????//因為是按月分表所以獲取下個月的時間判斷id是否是在臨界點創(chuàng)建的
????????//var?nextMonthFirstDay?=?ShardingCoreHelper.GetNextMonthFirstDay(DateTime.Now);//這個是錯誤的
????????var?nextMonthFirstDay?=?ShardingCoreHelper.GetNextMonthFirstDay(orderTime);
????????if?(orderTime.AddSeconds(10)?>?nextMonthFirstDay)
????????{
????????????var?nextTail?=?TimeFormatToTail(nextMonthFirstDay);
????????????return?DoArticleIdFilter(shardingOperator,?orderTime,?currentTail,?nextTail);
????????}
????????//因為是按月分表所以獲取這個月月初的時間判斷id是否是在臨界點創(chuàng)建的
????????//if?(orderTime.AddSeconds(-10)?
????????if?(orderTime.AddSeconds(-10)?????????{
????????????//上個月tail
????????????var?previewTail?=?TimeFormatToTail(orderTime.AddSeconds(-10));
????????????return?DoArticleIdFilter(shardingOperator,?orderTime,?previewTail,?currentTail);
????????}
????????return?DoArticleIdFilter(shardingOperator,?orderTime,?currentTail,?currentTail);
????}
????private?Expressionstring ,?bool>>?DoArticleIdFilter(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?CheckArticleId(string?orderNo,?out?DateTime?orderTime)
????{
????????//yyyyMMddHHmmss
????????if?(orderNo.Length?==?14)
????????{
????????????if?(DateTime.TryParseExact(orderNo,?"yyyyMMddHHmmss",?CultureInfo.InvariantCulture,
????????????????????DateTimeStyles.None,?out?var?parseDateTime))
????????????{
????????????????orderTime?=?parseDateTime;
????????????????return?true;
????????????}
????????}
????????orderTime?=?DateTime.MinValue;
????????return?false;
????}
}
完整路由:針對Id進行多字段分片并且支持大于小于排序
以上是多字段分片的優(yōu)化,詳情博客可以點擊這邊 .Net下你不得不看的分表分庫解決方案-多字段分片
然后我們繼續(xù)查詢看看結果

第三頁也是如此

DEMO:https://github.com/xuejmnet/ShardingWaterfallApp
總結
當前框架雖然是一個很年輕的框架,但是我相信我對其在分片領域的性能優(yōu)化應該在.net現有的所有框架下找不出第二個,并且框架整個也支持union all聚合,可以滿足列入group+first的特殊語句的查詢,又有很高的性能,一個不但是全自動分片而且還是高性能框架擁有非常多的特性性能,目標是榨干客戶端分片的最后一點性能。
最后
身位一個dotnet程序員我相信在之前我們的分片選擇方案除了mycat和shardingsphere-proxy外沒有一個很好的分片選擇,但是我相信通過ShardingCore 的原理解析,你不但可以了解到大數據下分片的知識點,更加可以參與到其中或者自行實現一個,我相信只有了解了分片的原理dotnet才會有更好的人才和未來,我們不但需要優(yōu)雅的封裝,更需要原理的是對原理了解。
我相信未來dotnet的生態(tài)會慢慢起來配上這近乎完美的語法
轉自:薛家明
鏈接:cnblogs.com/xuejiaming/p/15966501.html
