.Net使用分表分库框架ShardingCore实现多字段分片

目录
  • 介绍
    • 项目地址
  • 背景
  • 原理
  • 直接开始
    • 添加依赖
    • 创建一个订单对象
    • 创建DbContext
    • 创建分片路由
    • ShardingCore启动配置
  • 测试
    • 默认配置下的测试
    • 测试无路由返回默认值
  • 总结

介绍

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

dotnet下唯一一款全自动分表,多字段分表框架,拥有高性能,零依赖、零学习成本、零业务代码入侵,并且支持读写分离动态分表分库,同一种路由可以完全自定义的新星组件,通过本框架你不但可以学到很多分片的思想和技巧,并且更能学到Expression的奇思妙用

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

项目地址

github地址 https://github.com/xuejmnet/sharding-core

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

背景

直接开门见山,你有没有这种情况你需要将一批数据用时间分片来进行存储比如订单表,订单表的分片字段是订单的创建时间,并且id是雪花id订单编号是带时间信息的编号,因为.net下的所有分片方案几乎都是只支持单分片字段,所以当我们不使用分片字段查询也就是订单创建时间查询的话会带来全表查询,导致性能下降,譬如我想用雪花id或者订单编号进行查询,但是带来的却是内部低效的结果,针对这种情况是否有一个好的解决方案呢,有但是需要侵入业务代码,根据雪花id或者订单编号进行解析出对应的时间然后手动指定分片前提是框架支持手动指定.基于上述原因ShardingCore 带来了全新版本 x.3.2.x+ 支持多字段分片路由,并且拥有很完美的实现,废话不多说我们直接开始吧!!!!!!!!!!!

原理

我们现在假定一个很简单的场景,依然是订单时间按月分片,查询进行如下语句

//这边演示不使用雪花id因为雪花id很难在演示中展示所以使用订单编号进行演示格式: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();

上述语句OrderNo会查询Order_202112这张表,然后时间索引会查询......Order_202108、Order_202109、Order_202110,然后两者取一个交集我们发现其实是没有结果的,这个时候应该是返回默认值null或者直接报错

这就是一个简单的原理

直接开始

接下来我将用订单编号和创建时间来为大演示,数据库采用sqlserver(你也可以换成任意efcore支持的数据库),其中编号格式yyyyMMddHHmmss+new Random().Next(0,10000).ToString().PadLeft(4,'0'),创建时间是DateTime格式并且创建时间按月分表,这边不采用雪花id是因为雪花id的实现会根据workid和centerid的不一样而出现不一样的效果,接下来我们通过简单的5步操作实现多字段分片

添加依赖

首先我们添加两个依赖,一个是ShardingCore一个EFCore.SqlServer

//请安装最新版本目前x.3.2.x+,第一个版本号6代表efcore的版本号
Install-Package ShardingCore -Version 6.3.2

Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 6.0.1

创建一个订单对象

    public class Order
    {
        public string Id { get; set; }
        public string OrderNo { get; set; }
        public string Name { get; set; }
        public DateTime CreateTime { get; set; }
    }

创建DbContext

这边就简单的创建了一个dbcontext,并且设置了一下order如何映射到数据库,当然你可以采用attribute的方式而不是一定要fluentapi

    /// <summary>
    /// 如果需要支持分表必须要实现<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; }
    }

创建分片路由

这边我们采用订单创建时间按月分表

    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>
        /// 是否要在程序运行期间自动创建每月的表
        /// </summary>
        /// <returns></returns>
        public override bool AutoCreateTableByTime()
        {
            return true;
        }
        /// <summary>
        /// 分表从何时起创建
        /// </summary>
        /// <returns></returns>
        public override DateTime GetBeginTime()
        {
            return new DateTime(2021, 9, 1);
        }
        /// <summary>
        /// 配置额外分片路由规则
        /// </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>
        /// 订单编号的路由
        /// </summary>
        /// <param name="shardingKey"></param>
        /// <param name="shardingOperator"></param>
        /// <returns></returns>
        private Expression<Func<string, bool>> GetOrderNoRouteFilter(object shardingKey,
            ShardingOperatorEnum shardingOperator)
        {
            //将分表字段转成订单编号
            var orderNo = shardingKey?.ToString() ?? string.Empty;
            //判断订单编号是否是我们符合的格式
            if (!CheckOrderNo(orderNo, out var orderTime))
            {
                //如果格式不一样就直接返回false那么本次查询因为是and链接的所以本次查询不会经过任何路由,可以有效的防止恶意攻击
                return tail => false;
            }

            //当前时间的tail
            var currentTail = TimeFormatToTail(orderTime);
            //因为是按月分表所以获取下个月的时间判断id是否是在临界点创建的
            var nextMonthFirstDay = ShardingCoreHelper.GetNextMonthFirstDay(DateTime.Now);
            if (orderTime.AddSeconds(10) > nextMonthFirstDay)
            {
                var nextTail = TimeFormatToTail(nextMonthFirstDay);
                return DoOrderNoFilter(shardingOperator, orderTime, currentTail, nextTail);
            }
            //因为是按月分表所以获取这个月月初的时间判断id是否是在临界点创建的
            if (orderTime.AddSeconds(-10) < ShardingCoreHelper.GetCurrentMonthFirstDay(DateTime.Now))
            {
                //上个月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不应该被返回
                        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;
        }
    }

这边我来讲解一下为什么用额外字段分片需要些这么多代码呢,其实是这样的因为你是用订单创建时间CreateTime来进行分片的那么CreateTimeOrderNo的赋值原理上说应该在系统里面是不可能实现同一时间赋值的肯定有先后关系可能是几微妙甚至几飞秒,但是为了消除这种差异这边采用了临界点兼容算法来实现,让我们来看下一下代码

var order=new Order()
//执行这边生成出来的id是2021-11-30 23:59:59.999.999
order.OrderNo=DateTime.Now.ToString("yyyyMMddHHmmss")+"xxx";
//business code //具体执行时间不确定,哪怕没有business code也没有办法保证两者生成的时间一致,当然如果你可以做到一致完全不需要这么复杂的编写
............
//执行这边生成出来的时间是2021-12-01 00:00:00.000.000
order.CreateTime=DateTime.Now;

当然系统里面采用了前后添加10秒是一个比较保守的估算你可以采用前后一秒甚至几百毫秒都是ok的,具体业务具体实现,因为大部分的创建时间可能是由框架在提交后才会生成而不是new Order的时候,当然也不排除这种情况,当然如果你只需要考虑equal一种情况可以只编写equal的判断而不需要全部情况都考虑

ShardingCore启动配置

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启动时有哪些表
    .End();

var app = builder.Build();

// Configure the HTTP request pipeline.
app.Services.GetRequiredService<IShardingBootstrapper>().Start();

app.UseAuthorization();

app.MapControllers();

//额外添加一些种子数据
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();

整个配置下来其实也就两个地方需要配置还是相对比较简单的,直接启动开始我们的测试模式

测试

默认配置下的测试

        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--------------");

            //订单编号查询 精确定位
            Console.WriteLine("--------------Query OrderNo Begin--------------");
            var order2 = await _defaultDbContext.Set<Order>().Where(o=>o.OrderNo== "202110080000000003").FirstOrDefaultAsync();
            Console.WriteLine("--------------Query OrderNo End--------------");

            //创建时间查询 精确定位
            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--------------");

            //订单编号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--------------");

            //订单号和创建时间查询 精确定位 无路由结果 抛错或者返回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--------------");

            //非正确格式订单号 抛错或者返回default防止击穿数据库
            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();
        }

测试结果

测试结果非常完美除了无法匹配路由的时候那么我们该如何设置呢

测试无路由返回默认值

builder.Services.AddShardingDbContext<DefaultDbContext>(...)
    .Begin(o =>
    {
....
        o.ThrowIfQueryRouteNotMatch = false;//配置默认不抛出异常
    })

我们再次来看下测试结果

为何我们测试是不经过数据库直接查询,原因就是在我们做各个属性分片交集的时候返回了空那么框架会选择抛出异常或者返回默认值两种选项,并且我们在编写路由的时候判断格式不正确返回 return tail => false;直接让所有的交集都是空所以不会进行一次无意义的数据库查询

总结

看到这边你应该已经看到了本框架的强大之处,本框架不但可以实现多字段分片还可以实现自定义分片,而不是单单按时间分片这么简单,我完全可以设置订单从2021年后的订单按月分片,2021年前的订单按年分片,对于sharding-core而言这简直轻而易举,但是据我所知.Net下目前除了我没有任何一款框架可以做到真正的全自动分片+多字段分片,所以我们在设计框架分片的时候尽可能的将有用的信息添加到一些无意义的字段上比如Id可以有效的解决很多在大数据下发生的问题,你可以简单理解为我加了一个索引并且附带了额外列,我加了一个id并且带了分表信息在里面,也可以完全设计出一款附带分库的属性到id里面使其可以支持分表分库

demo地址 https://github.com/xuejmnet/MultiShardingProperties

到此这篇关于.Net使用分表分库框架ShardingCore实现多字段分片的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • .NET Core实现分表分库、读写分离的通用 Repository功能

    首先声明这篇文章不是标题党,我说的这个类库是 FreeSql.Repository,它作为扩展库现实了通用仓储层功能,接口规范参考 abp vnext 定义,实现了基础的仓储层(CURD). 安装 dotnet add package FreeSql.Repository 可用于:.net framework 4.6+..net core 2.1+ 定义 var fsql = new FreeSql.FreeSqlBuilder() .UseConnectionString(FreeSql.Da

  • 详解在.net core中完美解决多租户分库分表的问题

    前几天有人想做一个多租户的平台,每个租户一个库,可以进行水平扩展,应用端根据登录信息,切换到不同的租户库 计划用ef core实现,他们说做不出来,需要动态创建dbContext,不好实现 然而这个使用CRL很轻松就能解决了 以下为演示数据库,有两个库testdb和testdb2,查询结果如下 目标: 根据传入登录信息连不不同的库,查询返回结果,如登录人为01,返回d1.default,登录人为02 返回 d2.default 实际上这个需求就是分库分表的实现,通过设置数据库/表映射关系,根据传

  • .Net使用分表分库框架ShardingCore实现多字段分片

    目录 介绍 项目地址 背景 原理 直接开始 添加依赖 创建一个订单对象 创建DbContext 创建分片路由 ShardingCore启动配置 测试 默认配置下的测试 测试无路由返回默认值 总结 介绍 本期主角:ShardingCore 一款ef-core下高性能.轻量级针对分表分库读写分离的解决方案,具有零依赖.零学习成本.零业务代码入侵 dotnet下唯一一款全自动分表,多字段分表框架,拥有高性能,零依赖.零学习成本.零业务代码入侵,并且支持读写分离动态分表分库,同一种路由可以完全自定义的新

  • .Net极限生产力之分表分库全自动化Migrations Code-First

    目录 开始 移除静态容器 原生efcore 启动程序 添加todo字段并迁移 集成AbpVNext 新建两个接口用于赋值创建时间和guid AbpDbContext抽象类 新增分库分表路由 编写sqlserver分片迁移脚本生成 abp的efcore模块注入 启动abp迁移项目 集成Furion 新增todoitem 新增分表分库路由 新增分表路由 编写迁移文件 启动注入 添加迁移文件 集成WTM 添加依赖 新增分表分库路由 创建DbContextCreator 静态构造IShardingRun

  • 超大数据量存储常用数据库分表分库算法总结

    当一个应用的数据量大的时候,我们用单表和单库来存储会严重影响操作速度,如mysql的myisam存储,我们经过测试,200w以下的时候,mysql的访问速度都很快,但是如果超过200w以上的数据,他的访问速度会急剧下降,影响到我们webapp的访问速度,而且数据量太大的话,如果用单表存储,就会使得系统相当的不稳定,mysql服务很容易挂掉.所以当数据量超过200w的时候,建议系统工程师还是考虑分表. 以下是几种常见的分表算法. 1.按自然时间来分表/分库; 如一个应用的数据在一年后数据量会达到2

  • MySQL数据库优化之分表分库操作实例详解

    本文实例讲述了MySQL数据库优化之分表分库操作.分享给大家供大家参考,具体如下: 分表分库 垂直拆分 垂直拆分就是要把表按模块划分到不同数据库表中(当然原则还是不破坏第三范式),这种拆分在大型网站的演变过程中是很常见的.当一个网站还在很小的时候,只有小量的人来开发和维护,各模块和表都在一起,当网站不断丰富和壮大的时候,也会变成多个子系统来支撑,这时就有按模块和功能把表划分出来的需求.其实,相对于垂直切分更进一步的是服务化改造,说得简单就是要把原来强耦合的系统拆分成多个弱耦合的服务,通过服务间的

  • MySQL 分表分库怎么进行数据切分

    关系型数据库本身比较容易成为系统瓶颈,单机存储容量.连接数.处理能力都有限.当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从库.优化索引,做很多操作时性能仍下降严重.此时就要考虑对其进行切分了,切分的目的就在于减少数据库的负担,缩短查询时间. 数据库分布式核心内容无非就是数据切分(Sharding)以及切分后对数据的定位.整合.数据切分就是将数据分散存储到多个数据库中,使得单一数据库中的数据量变小,通过扩充主机的数量缓解单一数据库的性能问题,从而达到提升数据库操作性能的目

  • 使用ShardingSphere-Proxy实现分表分库

    目录 1.环境准备 2.数据库脚本准备 3.配置ShardingSphere-Proxy 分表原理解析 参考:Sharding-Proxy的基本功能使用 1. 环境准备 MySql 5.7 apache-shardingsphere-4.1.1-sharding-proxy-bin.tar.gz jdk 1.8 mysql-connector-java-5.1.49.jar 2. 数据库脚本准备 # 创建商品数据库 CREATE DATABASE IF NOT EXISTS `products`

  • mysql数据库分表分库的策略

    一.先说一下为什么要分表: 当一张的数据达到几百万时,你查询一次所花的时间会变多,如果有联合查询的话,有可能会死在那儿了.分表的目的就在于此,减小数据库的负担,缩短查询时间.日常开发中我们经常会遇到大表的情况,所谓的大表是指存储了百万级乃至千万级条记录的表.这样的表过于庞大,导致数据库在查询和插入的时候耗时太长,性能低下,如果涉及联合查询的情况,性能会更加糟糕.分表和表分区的目的就是减少数据库的负担,提高数据库的效率,通常点来讲就是提高表的增删改查效率.数据库中的数据量不一定是可控的,在未进行分

  • mysql分表分库的应用场景和设计方式

    很多朋友在论坛和留言区域问mysql在什么情况下才需要进行分库分表,以及采用何种设计方式才是最优的选择,根据这些问题,小编为大家整理了关于MySQL分库分表的应用场景和最优的设计方式举例. 一. 分表 场景:对于大型的互联网应用来说,数据库单表的记录行数可能达到千万级甚至是亿级,并且数据库面临着极高的并发访问.采用Master-Slave复制模式的MySQL架构, 只能够对数据库的读进行扩展,而对数据库的写入操作还是集中在Master上,并且单个Master挂载的Slave也不可能无限制多,Sl

  • 浅谈订单重构之 MySQL 分库分表实战篇

    目录 一.目标 二.环境准备 1.基本信息 2.数据库环境准备 3.建库 & 导入分表 三.配置&实践 1.pom文件 2.常量配置 3.yml 配置 4.分库分表策略 5.dao层编写 6.单元测试 四.总结 一.目标 本文将完成如下目标: 分表数量: 256    分库数量: 4 以用户ID(user_id) 为数据库分片Key 最后测试订单创建,更新,删除, 单订单号查询,根据user_id查询列表操作. 架构图: 表结构如下: CREATE TABLE `order_XXX` (

随机推荐