在实际应用开发中,有些项目可能数据量特别大,在系统应用一段时间后,性能随着数据量的增加会逐步下降,从而造成系统不定时卡顿等现象,在客户使用过程中也会产生不好的印象。在这种情况下,常规操作是增加索引,优化SQL语句等方案,这种常规操作可能会短暂的解决卡顿问题,但是随着数据量持续增多,效果反而越来越不明显。当常规操作逐渐不起作用的时候,我们就需要往更深层次的去考虑,比如分库,分表等缩减数据体量的方案。今天我们以一个简单的小例子,简述如何在ASP.NET Core WebApi程序中,通过引入ShardingCore组件进行分库分表等操作。
什么是ShardingCore?
ShardingCore是一款基于EntityFrameworkCore的高性能、轻量级针对分表分库读写分离的解决方案。它支持efcore2+的所有版本,支持efcore2+的所有数据库、支持自定义路由、动态路由、高性能分页、读写分离的一款组件,一款零依赖第三方组件的扩展,如果您熟悉efcore的使用那么对于这个组件您只需要简单配置即可零成本开始使用。
ShardingCore整体架构图,如下所示:
上图可知整体架构表现为以entity作为核心驱动来牵引整个框架。通过ShardingCore,可以实现同时兼容分库分表操作,还可以兼容部分表不分库分表等场景。
开发环境
在本示例中,开发环境如下:
- 开发工具:Visual Studio 2022。
- 项目框架:基于.NET8.0的ASP.NET WebApi项目。
- ORM框架:Entity Framework Core (EF Core) v9.0.6
- 分库分表组件:ShardingCore 7.9.1.24
操作步骤
使用用ShardingCore进行分库分表操作,主要步骤如下所示:
创建项目并安装组件
首先创建一个ASP.NET Core WebApi项目,然后安装ShardingCore组件。在Visual Studio 2022中,通过Nuget包管理器进行安装,当前最新版本为7.9.1.24,如下所示:
创建模型
我们以订单表(Order)为例,订单表包含订单Id,购买人,订单金额,所属区域,订单状态,创建时间等内容,如下所示:- namespace Okcoder.ShardingCore.Model
- {
- /// <summary>
- /// 订单表
- /// </summary>
- public class Order
- {
- /// <summary>
- /// 订单Id
- /// </summary>
- public string Id { get; set; }
- /// <summary>
- /// 付款人
- /// </summary>
- public string Payer { get; set; }
- /// <summary>
- /// 订单金额
- /// </summary>
- public long Money { get; set; }
- /// <summary>
- /// 所属区域
- /// </summary>
- public string Area { get; set; }
- /// <summary>
- /// 订单状态
- /// </summary>
- public OrderStatusEnum OrderStatus { get; set; }
- /// <summary>
- /// 创建时间
- /// </summary>
- public DateTime CreationTime { get; set; }
- }
- /// <summary>
- /// 订单状态枚举
- /// </summary>
- public enum OrderStatusEnum
- {
- NoPay = 1,
- Paying = 2,
- Payed = 3,
- PayFail = 4
- }
- }
复制代码
创建DbContext
EF-Core框架需要实现DbContext,在ShardingCore项目中,如果只分库,则仅需要继承自AbstractShardingDbContext;如果还需要分表,则需要实现IShardingTableDbContext接口。本示例需要同时实现分库分表,所以既需要继承自AbstractShardingDbContext,又需要实现IShardingTableDbContext。主要实现OnModelCreating方法,将模型Order和数据表进行映射,以及构造函数。如下所示:- using Microsoft.EntityFrameworkCore;
- using Okcoder.ShardingCore.Model;
- using ShardingCore.Core.VirtualRoutes.TableRoutes.RouteTails.Abstractions;
- using ShardingCore.Sharding.Abstractions;
- using ShardingCore.Sharding;
- namespace Okcoder.ShardingCore.DAL
- {
- public class DefaultShardingDbContext : AbstractShardingDbContext, IShardingTableDbContext
- {
- public DefaultShardingDbContext(DbContextOptions<DefaultShardingDbContext> options) : base(options)
- {
- }
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- base.OnModelCreating(modelBuilder);
- modelBuilder.Entity<Order>(entity =>
- {
- entity.HasKey(o => o.Id);
- entity.Property(o => o.Id).IsRequired().IsUnicode(false).HasMaxLength(50);
- entity.Property(o => o.Payer).IsRequired().IsUnicode(false).HasMaxLength(50);
- entity.Property(o => o.Area).IsRequired().IsUnicode(false).HasMaxLength(50);
- entity.Property(o => o.OrderStatus).HasConversion<int>();
- entity.ToTable(nameof(Order));
- });
- }
- /// <summary>
- /// 如果分表,则空实现即可
- /// </summary>
- public IRouteTail RouteTail { get; set; }
- }
- }
复制代码
创建虚拟分表路由
通过ShardingCore架构图可以看出,每一个表对应一个分表路由,同时ShardingCore默认实现了很多路由,这样可以直接继承,省去了很多麻烦,本示例主要继承自AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute,实现按月分表功能。主要定义分表的规则逻辑,包括分表起始时间,分表列,分表后缀等内容,具体如下所示:- using Okcoder.ShardingCore.Model;
- using ShardingCore.Core.EntityMetadatas;
- using ShardingCore.VirtualRoutes.Months;
- namespace Okcoder.ShardingCore.DAL
- {
- /// <summary>
- /// 路由构造函数支持依赖注入,依赖注入的对象生命周期必须是单例
- /// </summary>
- public class OrderVirtualTableRoute : AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute<Order>
- {
- /// <summary>
- /// 获取分表起始时间
- /// </summary>
- /// <returns></returns>
- public override DateTime GetBeginTime()
- {
- return new DateTime(2024, 1, 1);
- }
- /// <summary>
- /// 配置分表属性
- /// </summary>
- /// <param name="builder"></param>
- public override void Configure(EntityMetadataTableBuilder<Order> builder)
- {
- builder.ShardingProperty(o => o.CreationTime);
- //builder.AutoCreateTable(true);
- }
- /// <summary>
- /// 允许创建分表流程
- /// </summary>
- /// <returns></returns>
- public override bool AutoCreateTableByTime()
- {
- return true;
- }
- /// <summary>
- /// 分表后缀格式
- /// </summary>
- /// <param name="time"></param>
- /// <returns></returns>
- protected override string TimeFormatToTail(DateTime time)
- {
- return time.ToString("MM");
- }
- protected override bool RouteIgnoreDataSource => false;
- }
- }
复制代码 说明:虚拟路由就是联系虚拟表和物理表的中间介质,虚拟表在整个程序中只有一份,那么程序如何知道要查询系统哪一张表呢,最简单的方式就是通过虚拟表对应的路由IVirtualTableRoute 。
创建虚拟分库路由
分库路由继承自AbstractShardingOperatorVirtualDataSourceRoute,并实现抽象方法,主要包括动态创建数据源,以及路由Filter筛选函数,和ShardingKey和数据源映射方法,如下所示:- using Okcoder.ShardingCore.Model;
- using ShardingCore.Core.EntityMetadatas;
- using ShardingCore.Core.VirtualRoutes.DataSourceRoutes.Abstractions;
- using ShardingCore.Core.VirtualRoutes;
- using System.Collections.Concurrent;
- namespace Okcoder.ShardingCore.DAL
- {
- /// <summary>
- /// 分库路由
- /// </summary>
- public class OrderVirtualDbRoute : AbstractShardingOperatorVirtualDataSourceRoute<Order, DateTime>
- {
- private readonly ConcurrentBag<string> dataSources = new ConcurrentBag<string>();
- private readonly object _lock = new object();
- public OrderVirtualDbRoute():base()
- {
- for (int i = 0; i < 10; i++)
- {
- dataSources.Add(DateTime.Now.AddYears(i).ToString("yyyy"));
- }
- }
- public override bool AddDataSourceName(string dataSourceName)
- {
- var acquire = Monitor.TryEnter(_lock, TimeSpan.FromSeconds(3));
- if (!acquire)
- {
- return false;
- }
- try
- {
- var contains = dataSources.Contains(dataSourceName);
- if (!contains)
- {
- dataSources.Add(dataSourceName);
- return true;
- }
- }
- finally
- {
- Monitor.Exit(_lock);
- }
- return false;
- }
- public override void Configure(EntityMetadataDataSourceBuilder<Order> builder)
- {
- builder.ShardingProperty(o => o.CreationTime);
- }
- public override List<string> GetAllDataSourceNames()
- {
- return dataSources.ToList();
- }
- /// <summary>
- /// tail就是2020,2021,2022,2023,2024,2025 所以分片只需要格式化年就可以直接比较了
- /// </summary>
- /// <param name="shardingKey"></param>
- /// <param name="shardingOperator"></param>
- /// <returns></returns>
- public override Func<string, bool> GetRouteToFilter(DateTime shardingKey, ShardingOperatorEnum shardingOperator)
- {
- var t = $"{shardingKey:yyyyy}";
- switch (shardingOperator)
- {
- case ShardingOperatorEnum.GreaterThan:
- case ShardingOperatorEnum.GreaterThanOrEqual:
- return tail => String.Compare(tail, t, StringComparison.Ordinal) >= 0;
- case ShardingOperatorEnum.LessThan:
- {
- var currentYear = new DateTime(shardingKey.Year, 1, 1);
- //处于临界值 o=>o.time < [2021-01-01 00:00:00] 尾巴20210101不应该被返回
- if (currentYear == shardingKey)
- return tail => String.Compare(tail, t, StringComparison.Ordinal) < 0;
- return tail => String.Compare(tail, t, StringComparison.Ordinal) <= 0;
- }
- case ShardingOperatorEnum.LessThanOrEqual:
- return tail => String.Compare(tail, t, StringComparison.Ordinal) <= 0;
- case ShardingOperatorEnum.Equal: return tail => tail == t;
- default:
- {
- return tail => true;
- }
- }
- }
- public override string ShardingKeyToDataSourceName(object shardingKey)
- {
- return $"{shardingKey:yyyy}";//年份作为分库数据源名称
- }
- }
- }
复制代码 说明:数据源名称用来将对象路由到具体的数据源。
配置启动项
在Program的Main方法中配置ShardingCore启动项,主要配置默认数据源,扩展数据源,已经使用数据类型等内容。如下所示:- using Microsoft.EntityFrameworkCore;
- using Okcoder.ShardingCore.DAL;
- using ShardingCore;
- namespace Okcoder.ShardingCore
- {
- public class Program
- {
- public static void Main(string[] args)
- {
- var builder = WebApplication.CreateBuilder(args);
- // Add services to the container.
- builder.Services.AddControllers();
- builder.Services.AddShardingDbContext<DefaultShardingDbContext>()
- .UseRouteConfig(op =>
- {
- op.AddShardingDataSourceRoute<OrderVirtualDbRoute>();
- op.AddShardingTableRoute<OrderVirtualTableRoute>();
- }).UseConfig(op =>
- {
- op.UseShardingQuery((connStr, builder) =>
- {
- //connStr is delegate input param
- builder.UseSqlServer(connStr);
- });
- op.UseShardingTransaction((connection, builder) =>
- {
- //connection is delegate input param
- builder.UseSqlServer(connection);
- });
- op.AddDefaultDataSource("2024", builder.Configuration.GetConnectionString("Default"));
- op.AddExtraDataSource(sp =>
- {
- var dict = new Dictionary<string, string>();
- for (int i = 0; i < 10; i++)
- {
- var key = DateTime.Now.AddYears(i).ToString("yyyy");
- dict.Add(key, $"Server=localhost;Database=TestDb{key};Trusted_Connection=True;User Id=sa;Password=abc123;Encrypt=True;TrustServerCertificate=True;");
- }
- ;
- return dict;
- });
- }).AddShardingCore();
- var app = builder.Build();
- // Configure the HTTP request pipeline.
- app.UseHttpsRedirection();
- app.UseAuthorization();
- app.MapControllers();
- using (var scope = app.Services.CreateScope())
- {
- var testDbContext = scope.ServiceProvider.GetService<DefaultShardingDbContext>();
- testDbContext.Database.EnsureCreated();
- }
- app.Services.UseAutoTryCompensateTable();
- app.Run();
- }
- }
- }
复制代码 在上述示例中,最重要的实现自动创建分库,分表的是使用UseAutoTryCompensateTable()方法。
配置数据源
在appsettings.json文件中,配置默认数据源,如下所示:
创建控制器
在本示例中,为了测试,创建了OrderController,主要用于插入测试订单,如下所示:- using Microsoft.AspNetCore.Mvc;
- using Microsoft.EntityFrameworkCore;
- using Okcoder.ShardingCore.DAL;
- using Okcoder.ShardingCore.Model;
- namespace Okcoder.ShardingCore.Controllers
- {
- [ApiController]
- [Route("[controller]/[action]")]
- public class OrderController : ControllerBase
- {
- private readonly DefaultShardingDbContext dbContext;
- public OrderController(DefaultShardingDbContext dbContext)
- {
- this.dbContext = dbContext;
- }
- [HttpGet()]
- public string Add()
- {
- dbContext.Add(new Order()
- {
- Id = Guid.NewGuid().ToString("n"),
- Payer = "111",
- Area = "123",
- OrderStatus = OrderStatusEnum.Payed,
- Money = 100,
- CreationTime = DateTime.Now
- });
- dbContext.SaveChanges();
- return "Ok";
- }
- }
- }
复制代码
运行实例
通过上述步骤,可以实现按年分库,按月分表的功能,运行程序后,发现已经创建成功,如下所示:
调用Order/Add接口后,查看数据库后,发现数据已经成功插入,如下所示:
参考文档
本文主要参考官方文档等资料:
官方文档:https://gitee.com/hubo/sharding-core
以上就是《推荐一款基于EF-Core的分库分表利器》的全部内容,旨在抛砖引玉,一起学习,共同进步。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |