千家信息网

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

发表于:2024-11-14 作者:千家信息网编辑
千家信息网最后更新 2024年11月14日,小编给大家分享一下.Net如何使用分表分库框架ShardingCore实现多字段分片,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!原理我们现在假定一个很简单的场景,依然是订单时间按月
千家信息网最后更新 2024年11月14日.Net如何使用分表分库框架ShardingCore实现多字段分片

小编给大家分享一下.Net如何使用分表分库框架ShardingCore实现多字段分片,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!

原理

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

//这边演示不使用雪花id因为雪花id很难在演示中展示所以使用订单编号进行演示格式:yyyyMMddHHmmss+new Random().Next(0,10000).ToString().PadLeft(4,'0')            var dateTime = new DateTime(2021, 11, 1);            var order = await _myDbContext.Set().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.2Install-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

///     /// 如果需要支持分表必须要实现    ///     public class DefaultDbContext:AbstractShardingDbContext,IShardingTableDbContext    {        public DefaultDbContext(DbContextOptions options) : base(options)        {        }        protected override void OnModelCreating(ModelBuilder modelBuilder)        {            base.OnModelCreating(modelBuilder);            modelBuilder.Entity(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    {        ///         /// 配置主分表字段是CreateTime,额外分表字段是OrderNo        ///         ///         public override void Configure(EntityMetadataTableBuilder builder)        {            builder.ShardingProperty(o => o.CreateTime);            builder.ShardingExtraProperty(o => o.OrderNo);        }        ///         /// 是否要在程序运行期间自动创建每月的表        ///         ///         public override bool AutoCreateTableByTime()        {            return true;        }        ///         /// 分表从何时起创建        ///         ///         public override DateTime GetBeginTime()        {            return new DateTime(2021, 9, 1);        }        ///         /// 配置额外分片路由规则        ///         ///         ///         ///         ///         public override Expression> GetExtraRouteFilter(object shardingKey, ShardingOperatorEnum shardingOperator, string shardingPropertyName)        {            switch (shardingPropertyName)            {                case nameof(Order.OrderNo): return GetOrderNoRouteFilter(shardingKey, shardingOperator);                default: throw new NotImplementedException(shardingPropertyName);            }        }        ///         /// 订单编号的路由        ///         ///         ///         ///         private Expression> 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> 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.999order.OrderNo=DateTime.Now.ToString("yyyyMMddHHmmss")+"xxx";//business code //具体执行时间不确定,哪怕没有business code也没有办法保证两者生成的时间一致,当然如果你可以做到一致完全不需要这么复杂的编写............//执行这边生成出来的时间是2021-12-01 00:00:00.000.000order.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((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();    })    .AddTableEnsureManager(sp=>new SqlServerTableEnsureManager())//告诉ShardingCore启动时有哪些表    .End();var app = builder.Build();// Configure the HTTP request pipeline.app.Services.GetRequiredService().Start();app.UseAuthorization();app.MapControllers();//额外添加一些种子数据using (var serviceScope = app.Services.CreateScope()){    var defaultDbContext = serviceScope.ServiceProvider.GetService();    if (!defaultDbContext.Set().Any())    {        var orders = new List(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 Test1()        {             //订单名称全表扫描            Console.WriteLine("--------------Query Name Begin--------------");            var order1 = await _defaultDbContext.Set().Where(o=>o.Name=="Order3").FirstOrDefaultAsync();            Console.WriteLine("--------------Query Name End--------------");            //订单编号查询 精确定位            Console.WriteLine("--------------Query OrderNo Begin--------------");            var order2 = await _defaultDbContext.Set().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().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().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().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().Where(o => o.OrderNo == "a02110080000000003").FirstOrDefaultAsync();            Console.WriteLine("--------------Query OrderNo Not Check End--------------");            return Ok();        }

测试结果

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

测试无路由返回默认值

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

我们再次来看下测试结果

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

看完了这篇文章,相信你对".Net如何使用分表分库框架ShardingCore实现多字段分片"有了一定的了解,如果想了解更多相关知识,欢迎关注行业资讯频道,感谢各位的阅读!

0