框架介绍

依照惯例首先介绍本期主角:ShardingCore 一款ef-core下高性能、轻量级针对分表分库读写分离的解决方案,具有零依赖、零学习成本、零业务代码入侵

dotnet下唯一一款全自动分表,多字段分表框架,拥有高性能,零依赖、零学习成本、零业务代码入侵,并且支持读写分离动态分表分库,同一种路由可以完全自定义的新星组件框架

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

项目地址

  • github地址 https://github.com/dotnetcore/sharding-core

  • gitee地址 https://gitee.com/dotnetchina/sharding-core

背景

在大数据量下针对app端的瀑布流页面分页的优化实战,有大量的数据,前端需要以瀑布流的形式展示出来,我们最简单的就是以用户发布的文章为例,假设我们有大量的文章帖子被,需求需要按帖子的发布时间倒序展示给用户看,那么在手机端我们一般都是以下拉刷新,上拉加载的形式去展示,那么我们一般会有以下集中写法。

常规分页操作

select count(*) from article
select * from article order by publish_time desc limit 1,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....) twhere 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,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
......

流式分片下的优化

目前 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; }}
//dbcontextpublic 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属性才可以,这边设置的是名称一样也可以,因为考虑到匿名对象的selectbuilder.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;}//当前时间的tailvar 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)){//上个月tailvar 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){//yyyyMMddHHmmssif (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下你不得不看的分表分库解决方案-多字段分片

然后我们继续查询看看结果


第三页也是如此

总结

当前框架虽然是一个很年轻的框架,但是我相信我对其在分片领域的性能优化应该在.net现有的所有框架下找不出第二个,并且框架整个也支持union all聚合,可以满足列入group+first的特殊语句的查询,又有很高的性能,一个不但是全自动分片而且还是高性能框架拥有非常多的特性性能,目标是榨干客户端分片的最后一点性能。

MAKE DOTNET GREAT AGAIN

最后的最后

身位一个dotnet程序员我相信在之前我们的分片选择方案除了mycatshardingsphere-proxy外没有一个很好的分片选择,但是我相信通过ShardingCore 的原理解析,你不但可以了解到大数据下分片的知识点,更加可以参与到其中或者自行实现一个,我相信只有了解了分片的原理dotnet才会有更好的人才和未来,我们不但需要优雅的封装,更需要原理的是对原理了解。

我相信未来dotnet的生态会慢慢起来配上这近乎完美的语法

您的支持是开源作者能坚持下去的最大动力

.Net/C#分库分表高性能O(1)瀑布流分页相关推荐

  1. 怎么在同一页中分页_分库分表业界难题,跨库分页的几种常见方案

    为什么需要研究跨库分页? 互联网很多业务都有分页拉取数据的需求,例如: (1)微信消息过多时,拉取第N页消息: (2)京东下单过多时,拉取第N页订单: (3)浏览58同城,查看第N页帖子: 这些业务场 ...

  2. 分库分表下分页查询解决方案

    分库分表下分页查询解决方案 不管是随着业务量的增大.还是随着用户数量的增长,在单一表中无法承受大量大数据,导致查询速度极慢甚至拖垮数据库.所以分库分表的策略随之应用,但是如何在分库分表的情况下,进行分 ...

  3. 高性能数据库集群:分库分表

    读写分离分散了数据库读写操作的压力,但没有分散存储压力,当数据量达到千万甚至上亿条的时候,单台数据库服务器的存储能力会成为系统的瓶颈,主要体现在这几个方面: 数据量太大,读写的性能会下降,即使有索引, ...

  4. 高性能高可用MySQL(主从同步,读写分离,分库分表,去中心化,虚拟IP,心跳机制)

    高性能高可用MySQL(主从同步,读写分离,分库分表,去中心化,虚拟IP,心跳机制) 视频地址:https://www.bilibili.com/video/BV1ry4y1v7Tr?p=8& ...

  5. MySQL高性能:索引、锁、事务、分库分表如何撑起亿级数据

    最近项目增加,缺人手,面试不少,但匹配的人少的可怜.跟其他组的面试官聊,他也抱怨了一番,说候选人有点儿花拳绣腿,回答问题不落地,拿面试最常问的MySQL来说,并不只是懂"增删改查" ...

  6. 从原则、方案、策略及难点阐述分库分表

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/qq_34173549/article/details/79936693 一.分库分表原则 关系型数据 ...

  7. 面试官三连问:你这个数据量多大?分库分表怎么做?用的哪个组件?

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 1.  概述 ShardingSphere是一套开源的分布 ...

  8. 【转】MySQL分库分表环境下全局ID生成方案

    转载一篇博客,里面有很多的知识和思想值得我们去思考. ---------------------------------------------------------------------- 在大 ...

  9. 利用Mycat中间件实现RDS MySQL的分库分表及读写分离功能

    https://aws.amazon.com/cn/blogs/china/mycat-rds-mysql/ 随着移动互联网的兴起和大数据的蓬勃发展,系统的数据量正呈几何倍数增长,系统的压力也越来越大 ...

最新文章

  1. find——文件查找命令 linux一些常用命令
  2. 解决多线程安全问题-无非两个方法synchronized和lock 具体原理以及如何 获取锁AQS算法 (百度-美团)
  3. 世界首例针对特斯拉自动驾驶判罚:德国裁定Autopilot广告误导买家,特斯拉柏林工厂可能受阻...
  4. 关于lingo的@wrap函数
  5. mysql 存储过程月单拆天单_为部门整理的mysql_db使用军规
  6. 成功解决TypeError: unhashable type: 'numpy.ndarray'
  7. python pandas csv时间聚合_Python通过pandas操作excel常用功能
  8. commons-lang包中我们常用的类的作用
  9. 使用 Scala 写WordContext程序
  10. gradle groovy_适用于Java开发人员的Groovy吗? 认识Gradle,Grails和Spock
  11. python中int input_python中的input是什么
  12. Jupyter更改主题和字体及远程访问
  13. MySQL二十四:索引
  14. 程序员的恶性循环:加班-没空学习-老是写同等水平代码-无法提升代码质量-老是出BUG-老是需要修改-加班-......
  15. Android仿华为watch睡眠,华为watch2pro怎么查看睡眠状态
  16. 切换进程 切换线程
  17. 附录1. DXF组码说明
  18. 图搜索算法(一):图搜索的一般算法
  19. 【Vegas转】自信从自我欣赏开始
  20. imfindcircles

热门文章

  1. js中apply和join
  2. Mysql InnoDB Plugin安装 install
  3. css animation动画
  4. 32 commons-lang包学习
  5. 浅谈Java多线程同步机制之同步块(方法)——synchronized
  6. 教你如何在Android 6.0上创建系统悬浮窗
  7. eclipse中git插件配置 编辑
  8. android ota更新app,企业 OTA 更新  |  Android 开源项目  |  Android Open Source Project
  9. ad中电源插座怎么封装_您可以在房屋中安装的各种电源插座
  10. facebook 分享页面_Facebook个人资料,页面和组之间有什么区别?