框架介绍

按照常规首先介绍本期配角:ShardingCore 一款ef-core下高性能、轻量级针对分表分库读写拆散的解决方案,具备零依赖、零学习老本、零业务代码入侵

dotnet下惟一一款全自动分表,多字段分表框架,领有高性能,零依赖、零学习老本、零业务代码入侵,并且反对读写拆散动静分表分库,同一种路由能够齐全自定义的新星组件框架

你的star和点赞是我坚持下去的最大能源,一起为.net生态提供更好的解决方案

我的项目地址

  • github地址 https://github.com/dotnetcore...
  • gitee地址 https://gitee.com/dotnetchina...

背景

在大数据量下针对app端的瀑布流页面分页的优化实战,有大量的数据,前端须要以瀑布流的模式展现进去,咱们最简略的就是以用户公布的文章为例,假如咱们有大量的文章帖子被,需要须要按帖子的公布工夫倒序展现给用户看,那么在手机端咱们个别都是以下拉刷新,上拉加载的模式去展现,那么咱们个别会有以下集中写法。

惯例分页操作

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

这个操作是个别咱们的惯例分页操作,先进行total而后进行分页获取,这种做法的益处是反对任意规定的分页,毛病就是须要查问两次,一次count一次limit当然前期数据量切实太大能够只须要第一次count,然而也有一个问题就是如果数据量始终在变动会呈现下一次分页中还会有上一次的局部数据,因为数据在一直地新增,你的分页没跟上公布的速度那么就会有这个状况发送.

瀑布流分页

除了上述惯例分页操作外,咱们针对特定程序的分页也能够进行特定的分页形式来实现高性能,因为基于大前提咱们是大数量下的瀑布流,咱们的文章假如是以雪花id作为主键,那么咱们的分页能够这么写

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

首先咱们来剖析一下,这个语句是利用了插入的数据分布是程序和你须要查问的排序始终来实现的,又因为id不会反复并且雪花id的程序和工夫是统一的都是同向的所以能够利用这种形式来进行排序,limit每次不须要跳过任何数目,间接获取须要的数目即可,只须要传递上一次的查问后果的id即可,这个形式补救了上述惯例分页带来的问题,并且领有十分高的性能,然而毛病也不言而喻,不反对跳页,不反对任意排序,所以这个形式目前来说非常适合前端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而后机械能过滤,那么长处可想而知:简略,连接数耗费仅1个,sql语句反对的多,毛病也不言而喻,优化起来前期是个很大的问题,并且跨库下的应用有问题

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的所有的表进行并行的别离查问,而后针对每个查问的后果集进行优先级队列的排序后获取,长处:语句简略便于优化,性能可控,反对分库,毛病:实现简单,连接数耗费多

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

流式分片下的优化

目前 ShardingCore采纳的是流式聚合+union all,当且仅当用户手动3调用UseUnionAllMerge时会将分片sql转成union all 聚合。

针对上述瀑布流的分页ShardingCore是这么操作的

  • 确定分片表的程序,也就是因为分片字段是publish_time,又因为排序字段是publish_time所以分片表其实是有程序的,也就是[article_20220105、article_20220104、article_20220103、article_20220102、article_20220101],
    因为咱们是开启n个并发线程所以这个排序可能没有意义,然而如果咱们是仅开启设置单个连贯并发的时候,程序将当初通过id<last_id进行表筛选,之后顺次从大到小进行获取直到满足skip+take也就是0+20=20条数据后,进行间接摈弃残余查问返回后果,那么本次查问基本上就是和单表查问一样,因为基本上最多跨两张表根本能够满足要求(具体场景不肯定)
  • 阐明:假如last_id反解析进去的后果是2022-01-04 05:05:05那么能够基本上排除article_20220105,判断并发连接数如果是1那么间接查问article_20220104,如果不满足持续查问article_20220103,直到查问后果为20条如果并发连接数是2那么查问[article_20220104、article_20220103]如果不满足持续上面两张表直到获取到后果为20条数据,所以咱们能够很清晰的理解其工作原理并且来优化

阐明

  • 通过上述优化能够保障流式聚合查问在程序查问下的高性能O(1)
  • 通过上述优化能够保障客户端分片领有最小化连接数管制
  • 设置正当的主键能够无效的解决咱们在大数据分片下的性能优化

实际

ShardingCore目前针对分片查问进行了一直地优化和尽可能的无业务代码入侵来实现高性能分片查问聚合。

接下来我将为大家展现一款dotnet下惟一一款全自动路由、多字段分片、无代码入侵、高性能程序查问的框架在传统数据库畛域下的分片性能,如果你应用过我置信你肯定会爱上他。

第一步:装置依赖

# ShardingCore外围框架 版本6.4.2.4+PM> Install-Package ShardingCore# 数据库驱动这边抉择的是mysql的社区驱动 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; }    }//dbcontext    public class MyDbContext:AbstractShardingDbContext,IShardingTableDbContext    {        public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)        {//请勿增加会导致efcore 的model提前加载的办法如Database.xxxx        }        public IRouteTail RouteTail { get; set; }                public DbSet<Article> Articles { get; set; }    }

第三步:增加文章路由

    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(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<Article> builder)        {            //设置默认的框架针对Article的排序程序,这边设置的是倒序            builder.ShardingTailComparer(new TailDayReverseComparer());            ////如下设置和上述是一样的成果让框架真对Article的后缀排序应用倒序            //builder.ShardingTailComparer(Comparer<string>.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的线程数,这边优化到只须要一个线程即可,当然如果跨表那么就是串行执行            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(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<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进行分片路由的规定编写所以没方法进行对id的过滤,那么接下来咱们配置Id的分片规定

首先针对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(2022, 3, 1);        }        public override IEntityQueryConfiguration<Article> CreateEntityQueryConfiguration()        {            return new ArticleEntityQueryConfiguration();        }        public override Expression<Func<string, bool>> 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<string, 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是否是在临界点创立的            //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是否是在临界点创立的            //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<string, 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进行多字段分片并且反对大于小于排序

以上是多字段分片的优化,详情博客能够点击这边 .Net下你不得不看的分表分库解决方案-多字段分片

而后咱们持续查问看看后果


第三页也是如此

demo

DEMO

总结

以后框架尽管是一个很年老的框架,然而我置信我对其在分片畛域的性能优化应该在.net现有的所有框架下找不出第二个,并且框架整个也反对union all聚合,能够满足列入group+first的非凡语句的查问,又有很高的性能,一个不然而全自动分片而且还是高性能框架领有十分多的个性性能,指标是榨干客户端分片的最初一点性能。

MAKE DOTNET GREAT AGAIN

最初的最初

身位一个dotnet程序员我置信在之前咱们的分片抉择计划除了mycatshardingsphere-proxy外没有一个很好的分片抉择,然而我置信通过ShardingCore 的原理解析,你岂但能够理解到大数据下分片的知识点,更加能够参加到其中或者自行实现一个,我置信只有理解了分片的原理dotnet才会有更好的人才和将来,咱们岂但须要优雅的封装,更须要原理的是对原理理解。

我置信将来dotnet的生态会缓缓起来配上这近乎完满的语法

您的反对是开源作者能坚持下去的最大能源

  • Github ShardingCore
  • Gitee ShardingCore

    博客

QQ群:771630778

集体QQ:326308290(欢送技术支持提供您贵重的意见)

集体邮箱:326308290@qq.com