凤凰阁 發表於 2025-6-24 12:00:00

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

<p>在实际应用开发中,有些项目可能数据量特别大,在系统应用一段时间后,性能随着数据量的增加会逐步下降,从而造成系统不定时卡顿等现象,在客户使用过程中也会产生不好的印象。在这种情况下,常规操作是增加索引,优化SQL语句等方案,这种常规操作可能会短暂的解决卡顿问题,但是随着数据量持续增多,效果反而越来越不明显。当常规操作逐渐不起作用的时候,我们就需要往更深层次的去考虑,比如分库,分表等缩减数据体量的方案。今天我们以一个简单的小例子,简述如何在ASP.NET Core WebApi程序中,通过引入ShardingCore组件进行分库分表等操作。</p>
<p>&nbsp;</p>
<h1>什么是ShardingCore?</h1>
<p>&nbsp;</p>
<p>ShardingCore是一款基于EntityFrameworkCore的高性能、轻量级针对分表分库读写分离的解决方案。它支持efcore2+的所有版本,支持efcore2+的所有数据库、支持自定义路由、动态路由、高性能分页、读写分离的一款组件,一款零依赖第三方组件的扩展,如果您熟悉efcore的使用那么对于这个组件您只需要简单配置即可零成本开始使用。</p>
<p><img src="https://img2024.cnblogs.com/blog/1068941/202506/1068941-20250622233032186-654572026.png"></p>
<p>ShardingCore整体架构图,如下所示:</p>
<p><img src="https://img2024.cnblogs.com/blog/1068941/202506/1068941-20250622233220953-2046427410.png"></p>
<p>上图可知整体架构表现为以<code>entity</code>作为核心驱动来牵引整个框架。通过ShardingCore,可以实现同时兼容分库分表操作,还可以兼容部分表不分库分表等场景。</p>
<p>&nbsp;</p>
<h1>开发环境</h1>
<p>&nbsp;</p>
<p>在本示例中,开发环境如下:</p>
<ol>
<li>开发工具:Visual Studio 2022。</li>
<li>项目框架:基于.NET8.0的ASP.NET WebApi项目。</li>
<li>ORM框架:Entity Framework Core (EF Core)&nbsp; v9.0.6</li>
<li>分库分表组件:ShardingCore 7.9.1.24</li>
</ol>
<p>&nbsp;</p>
<h1>操作步骤</h1>
<p>&nbsp;</p>
<p>使用用ShardingCore进行分库分表操作,主要步骤如下所示:</p>
<p>&nbsp;</p>
<h2>创建项目并安装组件</h2>
<p>&nbsp;</p>
<p>首先创建一个ASP.NET Core WebApi项目,然后安装ShardingCore组件。在Visual Studio 2022中,通过Nuget包管理器进行安装,当前最新版本为7.9.1.24,如下所示:</p>
<p><img src="https://img2024.cnblogs.com/blog/1068941/202506/1068941-20250622234727507-992158899.png"></p>
<p>&nbsp;</p>
<h2>创建模型</h2>
<p>&nbsp;</p>
<p>我们以订单表(Order)为例,订单表包含订单Id,购买人,订单金额,所属区域,订单状态,创建时间等内容,如下所示:</p>
<pre class="language-csharp highlighter-hljs"><code>namespace Okcoder.ShardingCore.Model
{
    /// &lt;summary&gt;
    /// 订单表
    /// &lt;/summary&gt;
    public class Order
    {
      /// &lt;summary&gt;
      /// 订单Id
      /// &lt;/summary&gt;
      public string Id { get; set; }

      /// &lt;summary&gt;
      /// 付款人
      /// &lt;/summary&gt;
      public string Payer { get; set; }

      /// &lt;summary&gt;
      /// 订单金额
      /// &lt;/summary&gt;
      public long Money { get; set; }

      /// &lt;summary&gt;
      /// 所属区域
      /// &lt;/summary&gt;
      public string Area { get; set; }

      /// &lt;summary&gt;
      /// 订单状态
      /// &lt;/summary&gt;
      public OrderStatusEnum OrderStatus { get; set; }

      /// &lt;summary&gt;
      /// 创建时间
      /// &lt;/summary&gt;
      public DateTime CreationTime { get; set; }
    }

    /// &lt;summary&gt;
    /// 订单状态枚举
    /// &lt;/summary&gt;
    public enum OrderStatusEnum
    {
      NoPay = 1,
      Paying = 2,
      Payed = 3,
      PayFail = 4
    }
}</code></pre>
<p>&nbsp;</p>
<h2>创建DbContext</h2>
<p>&nbsp;</p>
<p>EF-Core框架需要实现DbContext,在ShardingCore项目中,如果只分库,则仅需要继承自AbstractShardingDbContext;如果还需要分表,则需要实现IShardingTableDbContext接口。本示例需要同时实现分库分表,所以既需要继承自AbstractShardingDbContext,又需要实现IShardingTableDbContext。主要实现OnModelCreating方法,将模型Order和数据表进行映射,以及构造函数。如下所示:</p>
<pre class="language-csharp highlighter-hljs"><code>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&lt;DefaultShardingDbContext&gt; options) : base(options)
      {
      }

      protected override void OnModelCreating(ModelBuilder modelBuilder)
      {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity&lt;Order&gt;(entity =&gt;
            {
                entity.HasKey(o =&gt; o.Id);
                entity.Property(o =&gt; o.Id).IsRequired().IsUnicode(false).HasMaxLength(50);
                entity.Property(o =&gt; o.Payer).IsRequired().IsUnicode(false).HasMaxLength(50);
                entity.Property(o =&gt; o.Area).IsRequired().IsUnicode(false).HasMaxLength(50);
                entity.Property(o =&gt; o.OrderStatus).HasConversion&lt;int&gt;();
                entity.ToTable(nameof(Order));
            });
      }
      /// &lt;summary&gt;
      /// 如果分表,则空实现即可
      /// &lt;/summary&gt;
      public IRouteTail RouteTail { get; set; }
    }
}</code></pre>
<p>&nbsp;</p>
<h2>创建虚拟分表路由</h2>
<p>&nbsp;</p>
<p>通过ShardingCore架构图可以看出,每一个表对应一个分表路由,同时ShardingCore默认实现了很多路由,这样可以直接继承,省去了很多麻烦,本示例主要继承自AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute,实现按月分表功能。主要定义分表的规则逻辑,包括分表起始时间,分表列,分表后缀等内容,具体如下所示:</p>
<pre class="language-csharp highlighter-hljs"><code>using Okcoder.ShardingCore.Model;
using ShardingCore.Core.EntityMetadatas;
using ShardingCore.VirtualRoutes.Months;

namespace Okcoder.ShardingCore.DAL
{
    /// &lt;summary&gt;
    /// 路由构造函数支持依赖注入,依赖注入的对象生命周期必须是单例
    /// &lt;/summary&gt;
    public class OrderVirtualTableRoute : AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute&lt;Order&gt;
    {
      /// &lt;summary&gt;
      /// 获取分表起始时间
      /// &lt;/summary&gt;
      /// &lt;returns&gt;&lt;/returns&gt;
      public override DateTime GetBeginTime()
      {
            return new DateTime(2024, 1, 1);
      }
      /// &lt;summary&gt;
      /// 配置分表属性
      /// &lt;/summary&gt;
      /// &lt;param name="builder"&gt;&lt;/param&gt;

      public override void Configure(EntityMetadataTableBuilder&lt;Order&gt; builder)
      {
            builder.ShardingProperty(o =&gt; o.CreationTime);
            //builder.AutoCreateTable(true);
      }
      /// &lt;summary&gt;
      /// 允许创建分表流程
      /// &lt;/summary&gt;
      /// &lt;returns&gt;&lt;/returns&gt;

      public override bool AutoCreateTableByTime()
      {
            return true;
      }

      /// &lt;summary&gt;
      /// 分表后缀格式
      /// &lt;/summary&gt;
      /// &lt;param name="time"&gt;&lt;/param&gt;
      /// &lt;returns&gt;&lt;/returns&gt;
      protected override string TimeFormatToTail(DateTime time)
      {
            return time.ToString("MM");
      }

      protected override bool RouteIgnoreDataSource =&gt; false;
    }
}</code></pre>
<p>说明:虚拟路由就是联系虚拟表和物理表的中间介质,虚拟表在整个程序中只有一份,那么程序如何知道要查询系统哪一张表呢,最简单的方式就是通过虚拟表对应的路由IVirtualTableRoute&nbsp;。</p>
<p>&nbsp;</p>
<h2>创建虚拟分库路由</h2>
<p>&nbsp;</p>
<p>分库路由继承自AbstractShardingOperatorVirtualDataSourceRoute,并实现抽象方法,主要包括动态创建数据源,以及路由Filter筛选函数,和ShardingKey和数据源映射方法,如下所示:</p>
<pre class="language-csharp highlighter-hljs"><code>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
{
    /// &lt;summary&gt;
    /// 分库路由
    /// &lt;/summary&gt;
    public class OrderVirtualDbRoute : AbstractShardingOperatorVirtualDataSourceRoute&lt;Order, DateTime&gt;
    {
      private readonly ConcurrentBag&lt;string&gt; dataSources = new ConcurrentBag&lt;string&gt;();

      private readonly object _lock = new object();

      public OrderVirtualDbRoute():base()
      {
            for (int i = 0; i &lt; 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&lt;Order&gt; builder)
      {
            builder.ShardingProperty(o =&gt; o.CreationTime);
      }

      public override List&lt;string&gt; GetAllDataSourceNames()
      {
            return dataSources.ToList();
      }

      /// &lt;summary&gt;
      /// tail就是2020,2021,2022,2023,2024,2025 所以分片只需要格式化年就可以直接比较了
      /// &lt;/summary&gt;
      /// &lt;param name="shardingKey"&gt;&lt;/param&gt;
      /// &lt;param name="shardingOperator"&gt;&lt;/param&gt;
      /// &lt;returns&gt;&lt;/returns&gt;
      public override Func&lt;string, bool&gt; GetRouteToFilter(DateTime shardingKey, ShardingOperatorEnum shardingOperator)
      {
            var t = $"{shardingKey:yyyyy}";

            switch (shardingOperator)
            {
                case ShardingOperatorEnum.GreaterThan:
                case ShardingOperatorEnum.GreaterThanOrEqual:
                  return tail =&gt; String.Compare(tail, t, StringComparison.Ordinal) &gt;= 0;
                case ShardingOperatorEnum.LessThan:
                  {
                        var currentYear = new DateTime(shardingKey.Year, 1, 1);
                        //处于临界值 o=&gt;o.time &lt; 尾巴20210101不应该被返回
                        if (currentYear == shardingKey)
                            return tail =&gt; String.Compare(tail, t, StringComparison.Ordinal) &lt; 0;
                        return tail =&gt; String.Compare(tail, t, StringComparison.Ordinal) &lt;= 0;
                  }
                case ShardingOperatorEnum.LessThanOrEqual:
                  return tail =&gt; String.Compare(tail, t, StringComparison.Ordinal) &lt;= 0;
                case ShardingOperatorEnum.Equal: return tail =&gt; tail == t;
                default:
                  {
                        return tail =&gt; true;
                  }
            }
      }

      public override string ShardingKeyToDataSourceName(object shardingKey)
      {
            return $"{shardingKey:yyyy}";//年份作为分库数据源名称
      }
    }
}</code></pre>
<p>&nbsp;说明:数据源名称用来将对象路由到具体的数据源。</p>
<p>&nbsp;</p>
<h2>配置启动项</h2>
<p>&nbsp;</p>
<p>在Program的Main方法中配置ShardingCore启动项,主要配置默认数据源,扩展数据源,已经使用数据类型等内容。如下所示:</p>
<pre class="language-csharp highlighter-hljs"><code>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&lt;DefaultShardingDbContext&gt;()
                .UseRouteConfig(op =&gt;
                {
                  op.AddShardingDataSourceRoute&lt;OrderVirtualDbRoute&gt;();
                  op.AddShardingTableRoute&lt;OrderVirtualTableRoute&gt;();
                }).UseConfig(op =&gt;
                {
                  op.UseShardingQuery((connStr, builder) =&gt;
                  {
                        //connStr is delegate input param
                        builder.UseSqlServer(connStr);
                  });
                  op.UseShardingTransaction((connection, builder) =&gt;
                  {
                        //connection is delegate input param
                        builder.UseSqlServer(connection);
                  });
                  op.AddDefaultDataSource("2024", builder.Configuration.GetConnectionString("Default"));
                  op.AddExtraDataSource(sp =&gt;
                  {
                        var dict = new Dictionary&lt;string, string&gt;();
                        for (int i = 0; i &lt; 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&lt;DefaultShardingDbContext&gt;();
                testDbContext.Database.EnsureCreated();
            }
            app.Services.UseAutoTryCompensateTable();
            app.Run();
      }
    }
}</code></pre>
<p>在上述示例中,最重要的实现自动创建分库,分表的是使用UseAutoTryCompensateTable()方法。</p>
<p>&nbsp;</p>
<h2>配置数据源</h2>
<p>&nbsp;</p>
<p>在appsettings.json文件中,配置默认数据源,如下所示:</p>
<p><img src="https://img2024.cnblogs.com/blog/1068941/202506/1068941-20250623221216658-1409847852.png"></p>
<p>&nbsp;</p>
<h2>创建控制器</h2>
<p>&nbsp;</p>
<p>在本示例中,为了测试,创建了OrderController,主要用于插入测试订单,如下所示:</p>
<pre class="language-csharp highlighter-hljs"><code>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";
      }
    }
}</code></pre>
<p>&nbsp;</p>
<h1>运行实例</h1>
<p>&nbsp;</p>
<p>通过上述步骤,可以实现按年分库,按月分表的功能,运行程序后,发现已经创建成功,如下所示:</p>
<p><img src="https://img2024.cnblogs.com/blog/1068941/202506/1068941-20250623220818771-303638983.png"></p>
<p>调用Order/Add接口后,查看数据库后,发现数据已经成功插入,如下所示:</p>
<p>&nbsp;<img src="https://img2024.cnblogs.com/blog/1068941/202506/1068941-20250623221633056-957138574.png"></p>
<p>&nbsp;</p>
<h1>参考文档</h1>
<p>&nbsp;</p>
<p>本文主要参考官方文档等资料:</p>
<p>官方文档:https://gitee.com/hubo/sharding-core</p>
<p>以上就是《推荐一款基于EF-Core的分库分表利器》的全部内容,旨在抛砖引玉,一起学习,共同进步。</p>

</div>
<div id="MySignature" role="contentinfo">
    <div id="AllanboltSignature">

    <p style="border-top: #e0e0e0 1px dashed; border-right: #e0e0e0 1px dashed; border-bottom: #e0e0e0 1px dashed; border-left: #e0e0e0 1px dashed; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 30px; font-family: 微软雅黑; font-size: 12px" id="PSignature">
<br>

   <img alt="" src="https://images.cnblogs.com/cnblogs_com/hsiang/1154298/o_115f1cd8.jpg" width="80px" height="80px">
   
    作者:老码识途
    <br>
    出处:http://www.cnblogs.com/hsiang/
    <br>
    本文版权归作者和博客园共有,写文不易,支持原创,欢迎转载【点赞】,转载请保留此段声明,且在文章页面明显位置给出原文连接,谢谢。
    <br>关注个人公众号,定时同步更新技术及职场文章
<br><br>
   </p>
</div><br><br>
来源:https://www.cnblogs.com/hsiang/p/18943535
頁: [1]
查看完整版本: 推荐一款基于EF-Core的分库分表利器