拓拔梨婷 发表于 2025-6-24 10:42:54

推荐一款基于EF-Core的分库分表利器

在实际应用开发中,有些项目可能数据量特别大,在系统应用一段时间后,性能随着数据量的增加会逐步下降,从而造成系统不定时卡顿等现象,在客户使用过程中也会产生不好的印象。在这种情况下,常规操作是增加索引,优化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 < 尾巴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
{
   
    /")]
    public class OrderController : ControllerBase
    {
      private readonly DefaultShardingDbContext dbContext;

      public OrderController(DefaultShardingDbContext dbContext)
      {
            this.dbContext = dbContext;
      }

      
      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的分库分表利器》的全部内容,旨在抛砖引玉,一起学习,共同进步。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 推荐一款基于EF-Core的分库分表利器