.NET平台企业级分库分表实战:基于WebAPI构建高性能数据架构

📅 发布时间:2026/8/21 19:35:52
.NET平台企业级分库分表实战:基于WebAPI构建高性能数据架构
在实际企业级应用中随着业务数据量的持续增长单库单表的性能瓶颈会日益凸显。此时分库分表成为解决海量数据存储与查询性能问题的核心架构方案。然而分库分表在带来水平扩展能力的同时也引入了数据路由、跨库查询、分布式事务等一系列复杂挑战。一个健壮的后端架构需要将这些挑战封装在底层向上层业务提供尽可能透明的数据访问接口。本文将围绕 .NET 平台基于 WebAPI 构建一个完整的企业级分库分表实战项目。我们将从零开始设计一个支持数据分片、路由分发和跨表查询的综合架构。通过这个项目你将掌握如何将分库分表的核心思想落地为可运行的代码理解从 SQL 解析到数据聚合的全链路并学会处理开发与生产环境中常见的各类问题。无论你是正在面临数据库性能压力的 .NET 开发者还是希望深入理解分布式数据存储架构的技术爱好者本文都将提供一条清晰的实践路径。1. 理解分库分表的核心概念与设计挑战在动手编码之前必须厘清分库分表要解决的根本问题以及随之而来的新问题。这决定了我们架构设计的边界和核心组件的职责。1.1 什么是分库分表分库分表是一种数据库水平拆分方案。其根本目的是通过将数据分散到多个数据库或数据表中来突破单机在存储容量、连接数、I/O 吞吐以及 CPU 处理能力上的瓶颈。分库将一个数据库中的数据按一定规则如用户ID取模拆分到多个物理或逻辑数据库中。每个库可以部署在不同的数据库服务器上从而实现存储和访问压力的分流。分表将一个表中的数据按一定规则拆分到同一个数据库的多个物理表中。这些表具有相同的表结构共同存储原表的数据。在实际项目中两者常结合使用例如先按业务分库再在库内按用户ID分表形成“分库分表”的两级拆分结构。1.2 分库分表带来的核心挑战引入分库分表后原本简单的单表 CRUD 操作变得复杂主要体现在以下几个方面SQL 路由应用程序执行一条 SQL 时框架必须能根据 SQL 中的条件通常是分片键如user_id准确计算出这条数据应该落在哪个库、哪张表并将 SQL 改写后发送到正确的目标。跨库/跨表查询对于不带分片键条件的查询如SELECT * FROM orders WHERE status PAID或者涉及聚合、排序、分页的查询需要向所有相关的分片发送查询然后在内存中进行数据的合并、排序、分页这个过程称为“聚合”或“归并”。分布式主键在分片环境下数据库自增 ID 会重复必须使用分布式 ID 生成算法如雪花算法来保证全局唯一。分布式事务一个业务操作可能涉及更新多个分片的数据如何保证这些更新的原子性全部成功或全部失败是一大难题。通常采用最终一致性方案替代强一致性。数据迁移与扩容当分片规则需要调整或数据量增长需要增加分片时如何平滑地进行数据迁移并保证迁移期间服务可用是运维层面的重大挑战。我们的实战项目将聚焦于解决前三个挑战构建一个能处理路由、跨片查询和分布式ID的 WebAPI 服务。对于分布式事务和数据迁移我们会在最佳实践部分给出方案选型建议。2. 项目环境准备与依赖配置我们将创建一个 ASP.NET Core WebAPI 项目并引入关键的分库分表中间件。选择 .NET 8 作为开发框架因为它提供了优异的性能和现代化的 API。2.1 开发环境与工具清单在开始前请确保你的开发环境满足以下要求组件要求说明.NET SDK8.0 或更高版本开发框架核心IDEVisual Studio 2022 或 VS Code推荐使用 Visual Studio 以获得更好的 .NET 开发体验数据库MySQL 8.0本文以 MySQL 为例其他数据库如 PostgreSQL原理类似包管理器NuGet.NET 的官方包管理器代码版本控制Git管理项目代码2.2 创建项目与引入核心依赖首先使用命令行或 IDE 创建一个新的 WebAPI 项目。dotnet new webapi -n ShardingDemo cd ShardingDemo接下来我们需要引入分库分表的核心组件。在 .NET 生态中ShardingCore是一个功能强大且活跃的开源分库分表框架。我们通过 NuGet 安装它。dotnet add package ShardingCore # 由于我们使用 MySQL还需要添加对应的数据库驱动和 ShardingCore 的 MySQL 扩展 dotnet add package Pomelo.EntityFrameworkCore.MySql dotnet add package ShardingCore.EntityFrameworkCore安装完成后你的ShardingDemo.csproj文件应包含类似以下的包引用Project SdkMicrosoft.NET.Sdk.Web PropertyGroup TargetFrameworknet8.0/TargetFramework ... /PropertyGroup ItemGroup PackageReference IncludeMicrosoft.AspNetCore.OpenApi Version8.0.0 / PackageReference IncludeShardingCore Version7.0.0 / !-- 版本号请以NuGet最新为准 -- PackageReference IncludePomelo.EntityFrameworkCore.MySql Version8.0.0 / PackageReference IncludeShardingCore.EntityFrameworkCore Version7.0.0 / PackageReference IncludeSwashbuckle.AspNetCore Version6.5.0 / /ItemGroup /Project2.3 数据库设计与分片规划为了演示我们设计一个简单的订单Order业务。假设我们的订单表数据量巨大需要分片。分片键我们选择UserId用户ID作为分片键。这意味着同一个用户的所有订单理论上会被路由到同一个分片上便于按用户维度查询。分片策略采用取模分片。例如我们计划分为 2 个数据库db0,db1每个库内再分为 2 张表order_0,order_1。那么总共有 4 个物理分片。分片算法UserId % 2决定数据库(UserId / 2) % 2决定表名。这只是一种示例策略实际策略可能更复杂。我们需要提前创建好这些物理数据库和表。执行以下 SQL 脚本以 MySQL 为例-- 创建数据库 CREATE DATABASE IF NOT EXISTS sharding_db0; CREATE DATABASE IF NOT EXISTS sharding_db1; USE sharding_db0; -- 在 db0 中创建两张订单表 CREATE TABLE IF NOT EXISTS order_0 ( Id VARCHAR(128) NOT NULL COMMENT 分布式主键, UserId BIGINT NOT NULL COMMENT 用户ID分片键, Amount DECIMAL(18,2) NOT NULL COMMENT 订单金额, Status INT NOT NULL DEFAULT 0 COMMENT 订单状态, CreateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (Id), INDEX IX_UserId (UserId) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表分片0; CREATE TABLE IF NOT EXISTS order_1 ( Id VARCHAR(128) NOT NULL COMMENT 分布式主键, UserId BIGINT NOT NULL COMMENT 用户ID分片键, Amount DECIMAL(18,2) NOT NULL COMMENT 订单金额, Status INT NOT NULL DEFAULT 0 COMMENT 订单状态, CreateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (Id), INDEX IX_UserId (UserId) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表分片1; USE sharding_db1; -- 在 db1 中同样创建两张订单表 CREATE TABLE IF NOT EXISTS order_0 ( Id VARCHAR(128) NOT NULL COMMENT 分布式主键, UserId BIGINT NOT NULL COMMENT 用户ID分片键, Amount DECIMAL(18,2) NOT NULL COMMENT 订单金额, Status INT NOT NULL DEFAULT 0 COMMENT 订单状态, CreateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (Id), INDEX IX_UserId (UserId) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表分片0; CREATE TABLE IF NOT EXISTS order_1 ( Id VARCHAR(128) NOT NULL COMMENT 分布式主键, UserId BIGINT NOT NULL COMMENT 用户ID分片键, Amount DECIMAL(18,2) NOT NULL COMMENT 订单金额, Status INT NOT NULL DEFAULT 0 COMMENT 订单状态, CreateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (Id), INDEX IX_UserId (UserId) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表分片1;注意主键Id使用了VARCHAR(128)这是为了兼容雪花算法等生成的字符串类型分布式ID。3. 构建分库分表数据访问层这一部分是架构的核心我们将定义数据模型、配置分片规则并实现数据访问上下文。3.1 定义实体与分布式ID生成首先在项目中创建Entities文件夹并添加Order实体类。// Entities/Order.cs using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace ShardingDemo.Entities { [Table(order)] // 这是逻辑表名物理表名由分片规则决定 public class Order { /// summary /// 订单ID使用分布式ID如雪花算法 /// /summary [Key] [StringLength(128)] public string Id { get; set; } /// summary /// 用户ID作为分片键 /// /summary public long UserId { get; set; } /// summary /// 订单金额 /// /summary [Column(TypeName decimal(18,2))] public decimal Amount { get; set; } /// summary /// 订单状态 (0:待支付1:已支付2:已取消) /// /summary public int Status { get; set; } /// summary /// 创建时间 /// /summary public DateTime CreateTime { get; set; } DateTime.Now; } }接下来我们需要一个生成分布式ID的工具。这里实现一个简单的雪花算法 ID 生成器。在生产环境中建议使用更成熟的开源库。// Utils/SnowflakeIdGenerator.cs using System; namespace ShardingDemo.Utils { public class SnowflakeIdGenerator { private static long _lastTimestamp -1L; private static long _sequence 0L; private static readonly long _workerIdBits 5L; private static readonly long _datacenterIdBits 5L; private static readonly long _maxWorkerId -1L ^ (-1L (int)_workerIdBits); private static readonly long _maxDatacenterId -1L ^ (-1L (int)_datacenterIdBits); private static readonly long _sequenceBits 12L; private static readonly long _workerIdShift _sequenceBits; private static readonly long _datacenterIdShift _sequenceBits _workerIdBits; private static readonly long _timestampLeftShift _sequenceBits _workerIdBits _datacenterIdBits; private static readonly long _sequenceMask -1L ^ (-1L (int)_sequenceBits); private static readonly object _lock new object(); private static readonly DateTime _epoch new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc); private readonly long _workerId; private readonly long _datacenterId; public SnowflakeIdGenerator(long workerId, long datacenterId) { if (workerId _maxWorkerId || workerId 0) throw new ArgumentException($worker Id cant be greater than {_maxWorkerId} or less than 0); if (datacenterId _maxDatacenterId || datacenterId 0) throw new ArgumentException($datacenter Id cant be greater than {_maxDatacenterId} or less than 0); _workerId workerId; _datacenterId datacenterId; } public string NextId() { lock (_lock) { var timestamp TimeGen(); if (timestamp _lastTimestamp) throw new InvalidOperationException($Clock moved backwards. Refusing to generate id for {_lastTimestamp - timestamp} milliseconds); if (_lastTimestamp timestamp) { _sequence (_sequence 1) _sequenceMask; if (_sequence 0) timestamp TilNextMillis(_lastTimestamp); } else { _sequence 0; } _lastTimestamp timestamp; var id ((timestamp - _epoch.Ticks / 10000) (int)_timestampLeftShift) | (_datacenterId (int)_datacenterIdShift) | (_workerId (int)_workerIdShift) | _sequence; return id.ToString(); } } private static long TilNextMillis(long lastTimestamp) { var timestamp TimeGen(); while (timestamp lastTimestamp) { timestamp TimeGen(); } return timestamp; } private static long TimeGen() { return (DateTime.UtcNow.Ticks - _epoch.Ticks) / 10000; // 转换为毫秒 } } }3.2 配置 DbContext 与分片规则这是整合ShardingCore的关键步骤。我们需要创建一个自定义的DbContext并为其配置分库、分表的路由规则。首先在Program.cs或Startup.cs中注册ShardingCore服务。这里以 .NET 8 的最小 API 为例。// Program.cs using Microsoft.EntityFrameworkCore; using ShardingCore; using ShardingCore.Bootstrappers; using ShardingDemo; using ShardingDemo.Data; var builder WebApplication.CreateBuilder(args); // 添加基础服务 builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // 1. 添加 ShardingCore 服务 builder.Services.AddShardingDbContextShardingDbContext() .AddEntityConfig(op { // 配置 Order 实体使用分表 op.AddShardingTableRouteOrderVirtualTableRoute(); // 配置数据源即分库路由 op.AddShardingDataSourceRouteOrderVirtualDataSourceRoute(); }) .AddConfig(op { // 配置默认连接字符串用于创建迁移等实际路由时会覆盖 op.UseShardingQuery((conn, dbContextBuilder) dbContextBuilder.UseMySql(conn, new MySqlServerVersion(new Version(8, 0, 0)))); op.UseShardingTransaction((conn, dbContextBuilder) dbContextBuilder.UseMySql(conn, new MySqlServerVersion(new Version(8, 0, 0)))); // 添加实际的数据源连接 op.AddDefaultDataSource(ds0, Serverlocalhost;Port3306;Databasesharding_db0;Uidroot;Pwdyour_password;); op.AddDefaultDataSource(ds1, Serverlocalhost;Port3306;Databasesharding_db1;Uidroot;Pwdyour_password;); }) .EnsureConfig(); // 确保配置生效 var app builder.Build(); // 配置 HTTP 请求管道 if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();接下来创建ShardingDbContext和路由规则。首先创建Data文件夹。// Data/ShardingDbContext.cs using Microsoft.EntityFrameworkCore; using ShardingCore.Core.VirtualRoutes.TableRoutes.RouteTails.Abstractions; using ShardingCore.Sharding; using ShardingCore.Sharding.Abstractions; using ShardingDemo.Entities; namespace ShardingDemo.Data { public class ShardingDbContext : AbstractShardingDbContext, IShardingDbContext { public ShardingDbContext(DbContextOptionsShardingDbContext options) : base(options) { } public DbSetOrder Orders { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 这里可以配置实体关系等但分片规则在路由类中定义 } // 实现接口用于在运行时获取路由信息 public IRouteTail RouteTail { get; set; } } }然后实现分库路由规则。这个规则决定一条数据根据UserId应该去哪个数据库。// Data/Routes/OrderVirtualDataSourceRoute.cs using ShardingCore.Core.VirtualRoutes.DataSourceRoutes.Abstractions; using ShardingCore.Core.VirtualRoutes.DataSourceRoutes.RouteRuleEngine; using ShardingCore.Exceptions; using ShardingCore.Extensions; using ShardingCore.VirtualRoutes.Abstractions; using System.Collections.Generic; using System.Linq; using ShardingDemo.Entities; namespace ShardingDemo.Data.Routes { public class OrderVirtualDataSourceRoute : AbstractVirtualDataSourceRouteOrder { // 所有物理数据源的名称 private readonly Liststring _dataSources new Liststring { ds0, ds1 }; public override Liststring GetAllDataSourceNames() { return _dataSources; } public override string RouteKey nameof(Order.UserId); // 指定分片键属性名 public override bool EnableRouteParseCompileCache true; // 核心路由方法根据分片键值计算目标数据源 public override string RouteWithValue(object shardingKey) { if (shardingKey null) throw new ShardingCoreException($sharding key is null); var userId Convert.ToInt64(shardingKey); // 简单取模路由userId % 2 var mod userId % 2; return $ds{mod}; } // 用于处理不带分片键的查询返回所有可能的数据源 public override DataSourceRouteResult RouteWithWhere(DataSourceRouteRuleContext routeRuleContext) { var queryable routeRuleContext.GetQueryable(); // 如果查询条件中不包含 UserId则需要遍历所有数据源 if (!queryable.HasFilter(x x.UserId)) { return new DataSourceRouteResult(GetAllDataSourceNames()); } // 如果包含 UserId则提取值并计算路由 var userIdValues queryable.GetFilterValueslong(x x.UserId); var dataSources userIdValues.Select(RouteWithValue).Distinct().ToList(); return new DataSourceRouteResult(dataSources); } } }接着实现分表路由规则。这个规则决定一条数据根据UserId应该去目标数据库中的哪张物理表。// Data/Routes/OrderVirtualTableRoute.cs using ShardingCore.Core.VirtualRoutes.TableRoutes.Abstractions; using ShardingCore.Core.VirtualRoutes.TableRoutes.RouteRuleEngine; using ShardingCore.Exceptions; using ShardingCore.Extensions; using ShardingCore.VirtualRoutes.Abstractions; using System.Collections.Generic; using System.Linq; using ShardingDemo.Entities; namespace ShardingDemo.Data.Routes { public class OrderVirtualTableRoute : AbstractVirtualTableRouteOrder { // 物理表后缀对应 order_0, order_1 private readonly Liststring _tableTails new Liststring { 0, 1 }; public override Liststring GetAllTails() { return _tableTails; } public override string RouteKey nameof(Order.UserId); public override bool EnableRouteParseCompileCache true; // 核心路由方法根据分片键值计算物理表后缀 public override string RouteWithValue(object shardingKey) { if (shardingKey null) throw new ShardingCoreException($sharding key is null); var userId Convert.ToInt64(shardingKey); // 表路由算法(userId / 2) % 2 // 这里除以2是为了与库路由%2配合形成4个分片。可根据业务调整。 var tableIndex (userId / 2) % 2; return tableIndex.ToString(); } // 物理表名生成规则逻辑表名 后缀 public override string PhysicTableToTail(string tableName) { // 假设逻辑表配置为 order物理表为 order_0 // 这里需要从物理表名 order_0 中提取出后缀 0 // ShardingCore 内部会处理通常只需返回后缀 return tableName.Split(_).Last(); } public override TableRouteResult RouteWithWhere(TableRouteRuleContext routeRuleContext) { var queryable routeRuleContext.GetQueryable(); if (!queryable.HasFilter(x x.UserId)) { // 无条件查询需要扫描所有表 return new TableRouteResult(GetAllTails().Select(tail new PhysicalTableInfo(tail)).ToList()); } var userIdValues queryable.GetFilterValueslong(x x.UserId); var tails userIdValues.Select(RouteWithValue).Distinct().Select(tail new PhysicalTableInfo(tail)).ToList(); return new TableRouteResult(tails); } } }至此数据访问层的核心配置已完成。ShardingCore框架会在运行时拦截对ShardingDbContext的查询根据上述路由规则将 SQL 改写并分发到正确的数据库和物理表上。4. 实现 WebAPI 业务层与控制器现在我们构建服务层和控制器向上提供透明的数据访问 API。4.1 创建仓储与服务首先创建一个通用的仓储接口和实现用于封装数据访问操作。// Services/IRepository.cs using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; namespace ShardingDemo.Services { public interface IRepositoryT where T : class { TaskT GetByIdAsync(string id); TaskIEnumerableT GetByConditionAsync(ExpressionFuncT, bool predicate); TaskIEnumerableT GetAllAsync(); Task AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(T entity); Taskbool ExistsAsync(ExpressionFuncT, bool predicate); IQueryableT GetQueryable(); } }// Services/Repository.cs using Microsoft.EntityFrameworkCore; using ShardingDemo.Data; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; namespace ShardingDemo.Services { public class RepositoryT : IRepositoryT where T : class { protected readonly ShardingDbContext _context; protected readonly DbSetT _dbSet; public Repository(ShardingDbContext context) { _context context; _dbSet context.SetT(); } public virtual async TaskT GetByIdAsync(string id) { return await _dbSet.FindAsync(id); } public virtual async TaskIEnumerableT GetByConditionAsync(ExpressionFuncT, bool predicate) { return await _dbSet.Where(predicate).ToListAsync(); } public virtual async TaskIEnumerableT GetAllAsync() { return await _dbSet.ToListAsync(); } public virtual async Task AddAsync(T entity) { await _dbSet.AddAsync(entity); await _context.SaveChangesAsync(); } public virtual async Task UpdateAsync(T entity) { _dbSet.Update(entity); await _context.SaveChangesAsync(); } public virtual async Task DeleteAsync(T entity) { _dbSet.Remove(entity); await _context.SaveChangesAsync(); } public virtual async Taskbool ExistsAsync(ExpressionFuncT, bool predicate) { return await _dbSet.AnyAsync(predicate); } public virtual IQueryableT GetQueryable() { return _dbSet.AsQueryable(); } } }然后创建订单服务注入仓储和 ID 生成器。// Services/OrderService.cs using ShardingDemo.Entities; using ShardingDemo.Utils; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace ShardingDemo.Services { public interface IOrderService { TaskOrder CreateOrderAsync(long userId, decimal amount); TaskOrder GetOrderAsync(string orderId); TaskListOrder GetOrdersByUserAsync(long userId); TaskListOrder GetOrdersByStatusAsync(int status); // 跨分片查询示例 Taskbool UpdateOrderStatusAsync(string orderId, int newStatus); } public class OrderService : IOrderService { private readonly IRepositoryOrder _orderRepository; private readonly SnowflakeIdGenerator _idGenerator; public OrderService(IRepositoryOrder orderRepository) { _orderRepository orderRepository; // 简单起见这里写死 workerId 和 datacenterId。生产环境应从配置读取。 _idGenerator new SnowflakeIdGenerator(1, 1); } public async TaskOrder CreateOrderAsync(long userId, decimal amount) { var order new Order { Id _idGenerator.NextId(), // 生成分布式ID UserId userId, Amount amount, Status 0, // 待支付 CreateTime DateTime.Now }; await _orderRepository.AddAsync(order); return order; } public async TaskOrder GetOrderAsync(string orderId) { return await _orderRepository.GetByIdAsync(orderId); } public async TaskListOrder GetOrdersByUserAsync(long userId) { // 带分片键的查询会被路由到特定分片效率高 var orders await _orderRepository.GetByConditionAsync(o o.UserId userId); return orders.ToList(); } public async TaskListOrder GetOrdersByStatusAsync(int status) { // 不带分片键的查询会扫描所有分片然后在内存中聚合结果 // 注意如果数据量极大这种查询性能很差需要额外设计。 var orders await _orderRepository.GetByConditionAsync(o o.Status status); return orders.OrderByDescending(o o.CreateTime).ToList(); // 内存排序 } public async Taskbool UpdateOrderStatusAsync(string orderId, int newStatus) { var order await _orderRepository.GetByIdAsync(orderId); if (order null) return false; order.Status newStatus; await _orderRepository.UpdateAsync(order); return true; } } }4.2 实现 API 控制器最后创建 WebAPI 控制器暴露订单的 CRUD 接口。// Controllers/OrdersController.cs using Microsoft.AspNetCore.Mvc; using ShardingDemo.Entities; using ShardingDemo.Services; using System; using System.Threading.Tasks; namespace ShardingDemo.Controllers { [ApiController] [Route(api/[controller])] public class OrdersController : ControllerBase { private readonly IOrderService _orderService; public OrdersController(IOrderService orderService) { _orderService orderService; } [HttpPost] public async TaskIActionResult CreateOrder([FromBody] CreateOrderRequest request) { if (request null || request.UserId 0 || request.Amount 0) { return BadRequest(Invalid request data.); } try { var order await _orderService.CreateOrderAsync(request.UserId, request.Amount); return Ok(new { OrderId order.Id, Message Order created successfully. }); } catch (Exception ex) { // 生产环境应使用结构化日志记录异常 return StatusCode(500, $Internal server error: {ex.Message}); } } [HttpGet({id})] public async TaskIActionResult GetOrder(string id) { var order await _orderService.GetOrderAsync(id); if (order null) { return NotFound(); } return Ok(order); } [HttpGet(user/{userId})] public async TaskIActionResult GetOrdersByUser(long userId) { var orders await _orderService.GetOrdersByUserAsync(userId); return Ok(orders); } [HttpGet(status/{status})] public async TaskIActionResult GetOrdersByStatus(int status) { // 状态范围验证 if (status 0 || status 2) { return BadRequest(Invalid status value.); } var orders await _orderService.GetOrdersByStatusAsync(status); return Ok(orders); } [HttpPatch({id}/status)] public async TaskIActionResult UpdateOrderStatus(string id, [FromBody] UpdateStatusRequest request) { if (request null || (request.NewStatus 0 || request.NewStatus 2)) { return BadRequest(Invalid request data.); } var success await _orderService.UpdateOrderStatusAsync(id, request.NewStatus); if (!success) { return NotFound(); } return Ok(new { Message Order status updated successfully. }); } } // 请求模型 public class CreateOrderRequest { public long UserId { get; set; } public decimal Amount { get; set; } } public class UpdateStatusRequest { public int NewStatus { get; set; } } }4.3 注册依赖与运行项目回到Program.cs注册我们创建的服务。// 在 Program.cs 的 builder.Services 配置部分添加 builder.Services.AddScoped(typeof(IRepository), typeof(Repository)); builder.Services.AddScopedIOrderService, OrderService(); // 注册 SnowflakeIdGenerator 为单例 builder.Services.AddSingletonSnowflakeIdGenerator(sp new SnowflakeIdGenerator(1, 1));现在启动项目。dotnet run项目启动后打开浏览器访问https://localhost:PORT/swaggerPORT 通常是 5000 或 7000你将看到自动生成的 Swagger API 文档。可以尝试调用POST /api/orders创建订单然后使用GET /api/orders/user/{userId}查询观察数据是否被正确插入到对应的分库分表中。5. 运行验证与结果分析通过 API 测试我们可以验证分库分表逻辑是否正常工作。5.1 验证数据路由创建订单使用 Swagger 或 Postman 调用POST /api/ordersBody 为{userId: 123, amount: 99.9}。框架会根据userId123计算路由123 % 2 1- 数据库ds1(123 / 2) % 2 61 % 2 1- 表order_1。因此这条数据应插入到sharding_db1.order_1表中。查询特定用户订单调用GET /api/orders/user/123。此查询带分片键userId123框架会精准路由到ds1.order_1执行查询效率最高。查询订单状态调用GET /api/orders/status/0。此查询不带分片键框架会向ds0.order_0,ds0.order_1,ds1.order_0,ds1.order_1四个物理表全部发送SELECT * FROM order WHERE status 0查询然后在应用程序内存中合并结果。你可以在数据库开启通用日志观察实际执行的 SQL 语句。5.2 观察 SQL 日志为了更直观地看到分片效果可以在 MySQL 中开启通用查询日志或者在ShardingCore配置中开启执行日志。在Program.cs的AddConfig部分添加日志配置.AddConfig(op { op.UseShardingQuery((conn, dbContextBuilder) dbContextBuilder .UseMySql(conn, new MySqlServerVersion(new Version(8, 0, 0))) .LogTo(Console.WriteLine, LogLevel.Information) // 将EF Core日志输出到控制台 .EnableSensitiveDataLogging()); // 生产环境慎用 // ... 其他配置 })重启应用后在控制台输出中你将看到类似以下的日志表明 SQL 被正确改写和路由Executing DbCommand [Parameters[p0?, p1?, ...], CommandTypeText, CommandTimeout30] INSERT INTO order_1 (Id, UserId, Amount, Status, CreateTime) VALUES (p0, p1, p2, p3, p4);6. 常见问题排查与优化实践在实际开发和部署中你会遇到各种问题。以下是一些典型场景的排查思路和优化建议。6.1 常见问题排查表问题现象可能原因检查方式处理建议启动时报错无法找到路由配置1. 路由类未在AddEntityConfig中注册。2. 路由类未继承正确的抽象类或实现接口。3. 程序集扫描失败。1. 检查Program.cs中AddShardingTableRoute和AddShardingDataSourceRoute的调用。2. 确认路由类继承自AbstractVirtualTableRouteT和AbstractVirtualDataSourceRouteT。3. 检查项目引用和命名空间。确保路由类被正确注册并且框架能加载到对应的程序集。可以尝试显式指定路由类所在的程序集。插入/查询数据时报错表不存在1. 物理表未按规则创建。2. 路由算法计算出错导致表名后缀不对。3. 连接字符串配置错误连到了错误的数据库。1. 登录对应数据库检查order_0,order_1等表是否存在。2. 在路由类的RouteWithValue方法中打印或记录计算出的分片键和目标。3. 检查AddDefaultDataSource中的连接字符串。核对分片算法与物理表创建脚本是否一致。确保所有数据源连接正常。跨分片查询性能极差查询条件中不包含分片键导致全表扫描。分析 API 调用和业务代码确认是否执行了GetOrdersByStatusAsync这类操作。查看数据库慢日志。1.业务设计规避尽可能让核心查询带上分片键。2.引入二级索引/查询表建立一张以status等字段为分片键的冗余表或ES索引。3.分页限制对跨片查询强制分页限制单次返回数据量。分布式ID冲突1. 雪花算法workerId或datacenterId配置重复。2. 系统时钟回拨。1. 检查多个服务实例的 ID 生成器配置。2. 监控服务器时间同步状态。1. 使用中心化的 ID 生成服务如 Redis Incr, 数据库序列或成熟的分布式ID组件如IdGenerator。2. 在雪花算法中处理时钟回拨例如等待或抛出异常。事务操作失败分库分表下跨分片的本地数据库事务无效。检查涉及更新多个分片数据的业务操作。1.业务规避设计上避免跨分片事务。2.使用分布式事务引入 Seata、DTM 等分布式事务框架或使用基于消息的最终一致性方案如本地消息表。6.2 生产环境最佳实践连接池与资源管理每个物理数据源都需要配置独立的连接池。确保DbContext的生命周期合理通常为 Scoped避免连接泄露。监控每个数据库的连接数、CPU 和 IO 使用率确保分片负载均衡。配置外置化将数据源连接字符串、分片数量、路由算法参数等配置移到appsettings.json或配置中心如 Apollo, Nacos。避免硬编码。监控与告警为分库分表中间件的关键操作如路由计算、SQL 改写、跨片查询添加 Metrics 指标并接入 Prometheus Grafana。对慢查询尤其是跨片查询设置告警。数据迁移与扩容预案设计之初就考虑好扩容方案。例如使用一致性哈希代替简单取模可以减少数据迁移量。准备在线数据迁移工具如ShardingCore可能提供的迁移模块或自研双写迁移脚本确保扩容时业务平滑。代码规范与审查在团队内明确规范禁止在业务代码中直接使用GetQueryable()后进行复杂的、不带分片键的 LINQ 查询。代码审查时重点关注数据访问层确保核心查询路径都使用了分片键。7. 架构扩展与进阶方向完成基础分库分表后可以考虑以下方向来完善你的企业级架构。7.1 读写分离在分片的基础上为每个主分片配置一个或多个只读副本将查询流量分摊到副本上。ShardingCore也支持读写分离配置需要在数据源路由中区分主从并在查询时选择从库。7.2 多租户与混合分片策略对于 SaaS 系统可以先按TenantId租户ID分库再在库内按UserId分表。这需要定义更复杂的复合分片路由规则。7.3 与分布式缓存集成将热点数据如用户信息、商品信息缓存到 Redis 等分布式缓存中减少对分片数据库的访问压力。注意缓存键的设计要包含分片信息例如order:{shard_key}:{order_id}。7.4 异步处理与最终一致性对于下单后需要更新多个分片或发送通知等操作可以引入消息队列如 RabbitMQ, Kafka。将核心下单操作与后续操作解耦通过消费消息来保证最终一致性避免长事务和性能瓶颈。分库分表是应对数据增长的有效手段但它也显著增加了系统的复杂度。成功的落地不仅依赖于选择一个合适的框架更在于前期的业务梳理、分片键的谨慎选择、对查询模式的深刻理解以及配套的监控运维体系的建设。通过本文的实战你已经拥有了一个可以运行和调试的起点接下来就是在真实的业务场景中不断迭代和优化你的分片架构。