狠狠色丁香婷婷综合尤物/久久精品综合一区二区三区/中国有色金属学报/国产日韩欧美在线观看 - 国产一区二区三区四区五区tv

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

C#.NET分庫分表高性能:瀑布流分頁

admin
2024年1月23日 11:41 本文熱度 746

框架介紹


依照慣例首先介紹本期主角:ShardingCore 一款ef-core下高性能、輕量級針對分表分庫讀寫分離的解決方案,具有零依賴、零學習成本、零業(yè)務(wù)代碼入侵。


dotnet下唯一一款全自動分表,多字段分表框架,擁有高性能,零依賴、零學習成本、零業(yè)務(wù)代碼入侵,并且支持讀寫分離動態(tài)分表分庫,同一種路由可以完全自定義的新星組件框架。


項目地址

github:https://github.com/dotnetcore/sharding-core

gitee:https://gitee.com/dotnetchina/sharding-core


背景


在大數(shù)據(jù)量下針對app端的瀑布流頁面分頁的優(yōu)化實戰(zhàn),有大量的數(shù)據(jù),前端需要以瀑布流的形式展示出來,我們最簡單的就是以用戶發(fā)布的文章為例,假設(shè)我們有大量的文章帖子被需求需要按帖子的發(fā)布時間倒序展示給用戶看,那么在手機端我們一般都是以下拉刷新,上拉加載的形式去展示,那么我們一般會有以下集中寫法。


常規(guī)分頁操作

select count(*) from article
select * from article order by publish_time desc limit 0,20

這個操作是一般我們的常規(guī)分頁操作,先進行total然后進行分頁獲取,這種做法的好處是支持任意規(guī)則的分頁,缺點就是需要查詢兩次,一次count、一次limit,當然后期數(shù)據(jù)量實在太大可以只需要第一次count,但是也有一個問題,就是如果數(shù)據(jù)量一直在變化,會出現(xiàn)下一次分頁中,還會有上一次的部分數(shù)據(jù),因為數(shù)據(jù)在不斷地新增,你的分頁沒跟上發(fā)布的速度那么就會有這個情況發(fā)生。

瀑布流分頁

除了上述常規(guī)分頁操作外,我們針對特定順序的分頁也可以進行特定的分頁方式來實現(xiàn)高性能,因為基于大前提我們是大數(shù)量下的瀑布流,我們的文章假設(shè)是以雪花id作為主鍵,那么我們的分頁可以這么寫:

select * from article where id<last_id order by publish_time desc limit 0,20

首先我們來分析一下,這個語句是利用了插入的數(shù)據(jù)分布是順序和你需要查詢的排序一致來實現(xiàn)的,又因為id不會重復,并且雪花id的順序和時間是一致的都是同向的,所以可以利用這種方式來進行排序,limit每次不需要跳過任何數(shù)目,直接獲取需要的數(shù)目即可,只需要傳遞上一次的查詢結(jié)果的id即可,這個方式彌補了上述常規(guī)分頁帶來的問題,并且擁有非常高的性能,但是缺點也顯而易見,不支持跳頁,不支持任意排序,所以這個方式目前來說非常適合前端app的瀑布流排序。

分片下的實現(xiàn)

首先分片下需要實現(xiàn)這個功能我們需要有id支持分片,并且publish_time按時間分表,兩者缺一不可。

原理

假設(shè)文章表article我們是以publish_time作為分片字段,假設(shè)按天分表,那么我們會擁有如下的表:

article_20220101、article_20220102、article_20220103、article_20220104、article_20220105、article_20220106......

雪花id輔助分片

因為雪花id可以反解析出時間,所以我們對雪花id的=,>=,>,<=,<,contains的操作都是可以進行輔助分片進行縮小分片范圍,假設(shè)我們的雪花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)點可想而知:簡單,連接數(shù)消耗僅1個,sql語句支持的多,缺點也顯而易見,優(yōu)化起來后期是個很大的問題,并且跨庫下的使用有問題。

select * from 
(select * from article_20220101 union all select * from article_20220102 union all select * from article_20220103....) t
 where id<last_id order by publish_time desc limit 0,20

流式分片,順序查詢

如果你是流式分片模式進行聚合,通常我們會將20220101-20220105的所有的表進行并行的分別查詢,然后針對每個查詢的結(jié)果集進行優(yōu)先級隊列的排序后獲取,優(yōu)點:語句簡單便于優(yōu)化、性能可控、支持分庫,缺點:實現(xiàn)復雜,連接數(shù)消耗多。

select * from article_20220101 where id<last_id order by publish_time desc limit 0,20
select * from article_20220102where id<last_id order by publish_time desc limit 0,20
select * from article_20220103 where id<last_id order by publish_time desc limit 0,20
......

流式分片下的優(yōu)化

目前 ShardingCore采用的是流式聚合+union all,當且僅當用戶手動3調(diào)用UseUnionAllMerge時會將分片sql轉(zhuǎn)成union all 聚合。

針對上述瀑布流的分頁ShardingCore是這么操作的:

  • 確定分片表的順序,也就是因為分片字段是publish_time,又因為排序字段是publish_time所以分片表其實是有順序的,也就是[article_20220105、article_20220104、article_20220103、article_20220102、article_20220101],因為我們是開啟n個并發(fā)線程所以這個排序可能沒有意義,但是如果我們是僅開啟設(shè)置單個連接并發(fā)的時候,程序?qū)F(xiàn)在通過id<last_id進行表篩選,之后依次從大到小進行獲取直到滿足skip+take,也就是0+20=20條數(shù)據(jù)后,進行直接拋棄剩余查詢返回結(jié)果,那么本次查詢基本上就是和單表查詢一樣,因為基本上最多跨兩張表基本可以滿足要求(具體場景不一定)。
  • 說明:假設(shè)last_id反解析出來的結(jié)果是2022-01-04 05:05:05,那么可以基本上排除article_20220105,判斷并發(fā)連接數(shù)如果是1,那么直接查詢article_20220104,如果不滿足繼續(xù)查詢article_20220103,直到查詢結(jié)果為20條;如果并發(fā)連接數(shù)是2,那么查詢[article_20220104、article_20220103],如果不滿足,繼續(xù)下面兩張表,直到獲取到結(jié)果為20條數(shù)據(jù),所以我們可以很清晰的了解其工作原理并且來優(yōu)化。

說明

  • 通過上述優(yōu)化可以保證流式聚合查詢在順序查詢下的高性能O(1)
  • 通過上述優(yōu)化可以保證客戶端分片擁有最小化連接數(shù)控制
  • 設(shè)置合理的主鍵可以有效的解決我們在大數(shù)據(jù)分片下的性能優(yōu)化

實踐

ShardingCore目前針對分片查詢進行了不斷地優(yōu)化和盡可能的無業(yè)務(wù)代碼入侵來實現(xiàn)高性能分片查詢聚合。

接下來我將為大家展示一款dotnet下唯一一款全自動路由、多字段分片、無代碼入侵、高性能順序查詢的框架在傳統(tǒng)數(shù)據(jù)庫領(lǐng)域下的分片功能,如果你使用過我相信你一定會愛上他。

第一步 安裝依賴

# ShardingCore核心框架 版本6.4.2.4+
PM> Install-Package ShardingCore
# 數(shù)據(jù)庫驅(qū)動這邊選擇的是mysql的社區(qū)驅(qū)動 efcore6最新版本即可
PM> Install-Package Pomelo.EntityFrameworkCore.MySql

第二步 添加對象和上下文

有很多朋友問我一定需要使用fluentapi來使用ShardingCore嗎?只是個人喜好,這邊我才用dbset+attribute來實現(xiàn):

//文章表
[Table(nameof(Article))]
public class Article
{
    [MaxLength(128)]
    [Key]
    public string Id { getset; }
    [MaxLength(128)]
    [Required]
    public string Title { getset; }
    [MaxLength(256)]
    [Required]
    public string Content { getset; }
    
    public DateTime PublishTime { getset; }
}
context
public class MyDbContext:AbstractShardingDbContext,IShardingTableDbContext
{
    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
    {
添加會導致efcore 的model提前加載的方法如Database.xxxx
    }

    public IRouteTail RouteTail { getset; }
    
    public DbSet<Article> Articles { getset; }
}

第三步 添加文章路由

public class ArticleRoute:AbstractSimpleShardingDayKeyDateTimeVirtualTableRoute<Article>
{
    public override void Configure(EntityMetadataTableBuilder<Article> builder)
    {
        builder.ShardingProperty(o => o.PublishTime);
    }

    public override bool AutoCreateTableByTime()
    {
        return true;
    }

    public override DateTime GetBeginTime()
    {
        return new DateTime(202231);
    }
}

到目前為止基本上Article已經(jīng)支持了按天分表。

第四步 添加查詢配置,讓框架知道我們是順序分表且定義分表的順序

public class TailDayReverseComparer : IComparer<string>
{
    public int Compare(string? x, string? y)
    {
        //程序默認使用的是正序也就是按時間正序排序我們需要使用倒序所以直接調(diào)用原生的比較器然后乘以負一即可
        return Comparer<string>.Default.Compare(x, y) * -1;
    }
}
//當前查詢滿足的復核條件必須是單個分片對象的查詢,可以join普通非分片表
public class ArticleEntityQueryConfiguration:IEntityQueryConfiguration<Article>
{
    public void Configure(EntityQueryBuilder<Article> builder)
    {
        //設(shè)置默認的框架針對Article的排序順序,這邊設(shè)置的是倒序
        builder.ShardingTailComparer(new TailDayReverseComparer());
        ////如下設(shè)置和上述是一樣的效果讓框架真對Article的后綴排序使用倒序
        //builder.ShardingTailComparer(Comparer<string>.Default, false);
        
        //簡單解釋一下下面這個配置的意思
        //第一個參數(shù)表名Article的哪個屬性是順序排序和Tail按天排序是一樣的這邊使用了PublishTime
        //第二個參數(shù)表示對屬性PublishTime asc時是否和上述配置的ShardingTailComparer一致,true表示一致,很明顯這邊是相反的因為默認已經(jīng)設(shè)置了tail排序是倒序
        //第三個參數(shù)表示是否是Article屬性才可以,這邊設(shè)置的是名稱一樣也可以,因為考慮到匿名對象的select
        builder.AddOrder(o => o.PublishTime, false,SeqOrderMatchEnum.Owner|SeqOrderMatchEnum.Named);
        //這邊為了演示使用的id是簡單的時間格式化所以和時間的配置一樣
        builder.AddOrder(o => o.Id, false,SeqOrderMatchEnum.Owner|SeqOrderMatchEnum.Named);
        //這邊設(shè)置如果本次查詢默認沒有帶上述配置的order的時候才用何種排序手段
        //第一個參數(shù)表示是否和ShardingTailComparer配置的一樣,目前配置的是倒序,也就是從最近時間開始查詢,如果是false就是從最早的時間開始查詢
        //后面配置的是熔斷器,也就是復核熔斷條件的比如FirstOrDefault只需要滿足一個就可以熔斷
        builder.AddDefaultSequenceQueryTrip(true, CircuitBreakerMethodNameEnum.Enumerator, CircuitBreakerMethodNameEnum.FirstOrDefault);

        //這邊配置的是當使用順序查詢配置的時候默認開啟的連接數(shù)限制是多少,startup一開始可以設(shè)置一個默認是當前cpu的線程數(shù),這邊優(yōu)化到只需要一個線程即可,當然如果跨表那么就是串行執(zhí)行
        builder.AddConnectionsLimit(1, LimitMethodNameEnum.Enumerator, LimitMethodNameEnum.FirstOrDefault);
    }
}

第五步 添加配置到路由

public class ArticleRoute:AbstractSimpleShardingDayKeyDateTimeVirtualTableRoute<Article>
{
    //省略.....
    public override IEntityQueryConfiguration<Article> 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<MyDbContext>()
    .AddEntityConfig(o =>
    {
        o.CreateShardingTableOnStart = true;
        o.EnsureCreatedWithOutShardingTable = true;
        o.AddShardingTableRoute<ArticleRoute>();
    })
    .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<MyDbContext>());
    }).EnsureConfig();

var app = builder.Build();

app.Services.GetRequiredService<IShardingBootstrapper>().Start();
using (var scope = app.Services.CreateScope())
{
    var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
    if (!myDbContext.Articles.Any())
    {
        List<Article> articles = new List<Article>();
        var beginTime = new DateTime(20223111,1);
        for (int i = 0; i < 70; i++)
        {
            var article = new Article();
            article.Id = beginTime.ToString("yyyyMMddHHmmss");
            article.Title = "標題" + i;
            article.Content = "內(nèi)容" + 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<IActionResult> 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<Article> builder)
    {
        builder.ShardingProperty(o => o.PublishTime);
        builder.ShardingExtraProperty(o => o.Id);
    }

    public override bool AutoCreateTableByTime()
    {
        return true;
    }

    public override DateTime GetBeginTime()
    {
        return new DateTime(202231);
    }

    public override IEntityQueryConfiguration<Article> CreateEntityQueryConfiguration()
    {
        return new ArticleEntityQueryConfiguration();
    }

    public override Expression<Func<stringbool>> GetExtraRouteFilter(object shardingKey, ShardingOperatorEnum shardingOperator, string shardingPropertyName)
    {
        switch (shardingPropertyName)
        {
            case nameof(Article.Id): return GetArticleIdRouteFilter(shardingKey, shardingOperator);
        }

      return base.GetExtraRouteFilter(shardingKey, shardingOperator, shardingPropertyName);
    }
    /// <summary>
    /// 文章id的路由
    /// </summary>
    /// <param name="shardingKey"></param>
    /// <param name="shardingOperator"></param>
    /// <returns></returns>
    private Expression<Func<stringbool>> GetArticleIdRouteFilter(object shardingKey,
        ShardingOperatorEnum shardingOperator)
    {
        //將分表字段轉(zhuǎn)成訂單編號
        var id = shardingKey?.ToString() ?? string.Empty;
        //判斷訂單編號是否是我們符合的格式
        if (!CheckArticleId(id, out var orderTime))
        {
            //如果格式不一樣就直接返回false那么本次查詢因為是and鏈接的所以本次查詢不會經(jīng)過任何路由,可以有效的防止惡意攻擊
            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) < ShardingCoreHelper.GetCurrentMonthFirstDay(DateTime.Now))//這個是錯誤的
        if (orderTime.AddSeconds(-10) < ShardingCoreHelper.GetCurrentMonthFirstDay(orderTime))
        {
            //上個月tail
            var previewTail = TimeFormatToTail(orderTime.AddSeconds(-10));

            return DoArticleIdFilter(shardingOperator, orderTime, previewTail, currentTail);
        }

        return DoArticleIdFilter(shardingOperator, orderTime, currentTail, currentTail);

    }

    private Expression<Func<stringbool>> 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不應(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 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)化,然后我們繼續(xù)查詢看看結(jié)果:

第三頁也是如此


DEMO:https://github.com/xuejmnet/ShardingWaterfallApp


總結(jié)

當前框架雖然是一個很年輕的框架,但是相信對其在分片領(lǐng)域的性能優(yōu)化應(yīng)該在.net現(xiàn)有的所有框架下找不出第二個,并且框架整個也支持union all聚合,可以滿足列入group+first的特殊語句的查詢,又有很高的性能,一個不但是全自動分片而且還是高性能框架,擁有非常多的特性性能,目標是榨干客戶端分片的最后一點性能。

最后

身位一個dotnet程序員,我相信在之前我們的分片選擇方案除了mycatshardingsphere-proxy外,沒有一個很好的分片選擇,但是我相信通過ShardingCore 的原理解析,你不但可以了解到大數(shù)據(jù)下分片的知識點,更加可以參與到其中或者自行實現(xiàn)一個,我相信只有了解了分片的原理,dotnet才會有更好的人才和未來,我們不但需要優(yōu)雅的封裝,更需要的是對原理了解。

我相信未來dotnet的生態(tài)會慢慢起來配上這近乎完美的語法。

轉(zhuǎn)自:薛家明

鏈接:https://cnblogs.com/xuejiaming/p/15966501.html


該文章在 2024/1/23 11:41:12 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點晴ERP是一款針對中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國內(nèi)大量中小企業(yè)的青睞。
點晴PMS碼頭管理系統(tǒng)主要針對港口碼頭集裝箱與散貨日常運作、調(diào)度、堆場、車隊、財務(wù)費用、相關(guān)報表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點,圍繞調(diào)度、堆場作業(yè)而開發(fā)的。集技術(shù)的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點晴WMS倉儲管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質(zhì)期管理,貨位管理,庫位管理,生產(chǎn)管理,WMS管理系統(tǒng),標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務(wù)都免費,不限功能、不限時間、不限用戶的免費OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved