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


在实际应用开发中,有些项目可能数据量特别大,在系统应用一段时间后,性能随着数据量的增加会逐步下降,从而造成系统不定时卡顿等现象,在客户使用过程中也会产生不好的印象。在这种情况下,常规操作是增加索引,优化SQL语句等方案,这种常规操作可能会短暂的解决卡顿问题,但是随着数据量持续增多,效果反而越来越不明显。当常规操作逐渐不起作用的时候,我们就需要往更深层次的去考虑,比如分库,分表等缩减数据体量的方案。今天我们以一个简单的小例子,简述如何在ASP.NET Core WebApi程序中,通过引入ShardingCore组件进行分库分表等操作。

 

什么是ShardingCore?

 

ShardingCore是一款基于EntityFrameworkCore的高性能、轻量级针对分表分库读写分离的解决方案。它支持efcore2+的所有版本,支持efcore2+的所有数据库、支持自定义路由、动态路由、高性能分页、读写分离的一款组件,一款零依赖第三方组件的扩展,如果您熟悉efcore的使用那么对于这个组件您只需要简单配置即可零成本开始使用。

ShardingCore整体架构图,如下所示:

上图可知整体架构表现为以entity作为核心驱动来牵引整个框架。通过ShardingCore,可以实现同时兼容分库分表操作,还可以兼容部分表不分库分表等场景。

 

开发环境

 

在本示例中,开发环境如下:

  1. 开发工具:Visual Studio 2022。
  2. 项目框架:基于.NET8.0的ASP.NET WebApi项目。
  3. ORM框架:Entity Framework Core (EF Core)  v9.0.6
  4. 分库分表组件: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的分库分表利器》的全部内容,旨在抛砖引玉,一起学习,共同进步。