news 2026/9/7 14:00:57

.NET分库分表实战:从分片键设计到ShardingCore落地全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
.NET分库分表实战:从分片键设计到ShardingCore落地全指南

很多 .NET 开发者在业务体量涨到一定阶段后,都会遇到同一个问题:数据库成了整个系统的短板。不是 SQL 写得不好,不是索引没建对,而是在数据量达到千万级、亿级之后,单库单表已经承载不住读写压力。这时候,分库分表就成了绕不开的话题。

但 .NET 生态的分库分表资料,确实比 Java 生态少很多。Java 有 ShardingSphere 这样成熟的框架,文档和案例都很丰富;.NET 这边虽然也有不少方案,但中文资料相对零散,真要落地时,很容易被分片键选择、跨分片分页、联表查询、数据合并这些问题卡住。

这篇文章我会从实际项目出发,讲清楚几件事:分库分表到底解决了什么;数据量大了之后,分页、联表查询、数据拆分合并到底怎么处理;在 AI 辅助编程流行的今天,怎么让 AI 帮我们更快地完成方案设计与代码落地。文章里的代码和配置都偏向实战,建议收藏备用。

先给出我的明确判断:分库分表不是银弹,没有到“不得不做”的规模,尽量不要碰。但如果业务确实到了这一步,那这篇文章可以帮你少走很多弯路。

1. 高并发场景下,数据库瓶颈究竟出在哪

先说一个真实存在的开发场景。假设你负责一个订单系统,上线初期每天几千单,一张orders表轻松搞定,索引建得合理,查询响应时间都在几十毫秒以内。但业务增长后,订单量从每天几千涨到几十万,历史数据不断累积。半年之后,orders表里有了上千万行数据。这时候你会陆续发现:

  • 大范围查询耗时从几十毫秒涨到几百毫秒,甚至秒级;
  • 写入请求一多,数据库锁竞争让整体吞吐量大幅下降;
  • 夜间统计任务和白天交易高峰期重叠时,数据库连接数和 CPU 双双告警;
  • 即使建了索引,索引体积变大之后,内存命中率下降,磁盘 IO 成为新瓶颈。

很多人第一反应是加索引、加缓存、做读写分离。这些手段在数据量还没到极端规模的时候确实有效,但当单表数据量持续膨胀,索引体积变大,缓存命中率下降,读写分离也只能缓解读压力,写压力依然集中在主库,单表瓶颈依然存在。

分库分表的本质是“拆”。把一个大库拆成多个库,把一张大表拆成多张小表,让每一份数据都落在更小的物理集合里。这样一来,单表的数据量降下来了,单库的负载降下来了,系统整体的吞吐能力也就上去了。

这个思路听起来很简单,但真正落地时,会有大量细节问题。这也是为什么很多团队“想到”分库分表和“做成”分库分表之间,常常隔着好几个月的加班时间。

2. 分库分表的两种拆分路径:垂直与水平

分库分表通常分为两种路径:垂直拆分和水平拆分。很多新手容易混淆,这里先用最通俗的方式讲清楚。

2.1 垂直拆分

垂直拆分是按业务模块拆,把一张宽表或者一个库里的不同业务拆分到不同库。典型场景是“用户库、订单库、商品库”拆开。原来是单体数据库包含所有表,现在按照业务域拆成专门的服务和数据库。

垂直拆分的优点是:

  • 业务边界清晰,不同团队可以独立维护自己的库;
  • 单库的连接数、IO、锁竞争压力立刻下降;
  • 为微服务化打基础。

缺点是:

  • 原来一个事务里可以同时操作订单和商品表,拆库后事务变成了分布式事务;
  • 跨业务查询不能再用 SQL join,必须走应用层聚合或者接口调用。

很多微服务项目做“数据库拆分”时,指的就是垂直拆分。

2.2 水平拆分

水平拆分是按数据行拆,把同一张表的数据按某个规则分散到多张结构相同的表,或者多个结构相同的库。比如用户 ID 取模,把订单数据分散到orders_0orders_1orders_2orders_3四张表里。

水平拆分解决的是单表数据量过大、单库写入吞吐不足的问题。它不会像垂直拆分那样改变业务边界,但会把单表的 CRUD 变成“先路由、再操作”。每一条 SQL 进来,先要判断应该落到哪张表、哪个库。

两张方式不是对立的。在实际大型项目中,通常会先做垂直拆分,让每个业务域独立,再对核心的订单表、流水表做水平拆分。下面这张表可以帮助你快速判断:

拆分方式解决的核心问题典型场景主要成本
垂直拆分单库连接数、业务耦合、团队协作冲突微服务改造、业务域隔离分布式事务、跨业务聚合
水平拆分单表数据量、单库写入吞吐瓶颈订单表、流水表、日志表分片路由、扩容、跨分片查询

这里要特别提醒:垂直拆分相对容易,因为本质是“数据库架构调整 + 代码调用方式调整”;水平拆分难度更大,因为它直接改变 SQL 的执行路径,所有查询都要经过一层分片路由。

3. .NET 生态分库分表选型:中间件还是自研

在 .NET 生态里做分库分表,目前没有像 Java ShardingSphere 那样的“全家桶”方案,但可以选择的路线并不少。我按实际项目的经验把选型分为三类。

3.1 使用通用数据中间件

比如 ShardingCore,这是 .NET 生态里比较有代表性的分库分表中间件。它基于 EF Core 扩展,支持按时间、按哈希等方式分片,也支持跨分片分页和聚合查询。优点是接入成本相对低,和 EF Core 一起使用时,大部分 LINQ 查询不用改。

这类中间件适合大多数业务系统,尤其是已经使用 EF Core 的团队。需要注意的是,任何中间件都有能力边界,框架能帮你解析查询、路由到分片,但它不会帮你设计分片键,也不解决所有 SQL 兼容问题。

3.2 代理层方案

类似 ShardingSphere-Proxy 的方式,在应用和数据库之间加一层代理。应用连接的还是普通数据库地址,代理层负责解析 SQL、路由到后端的多个数据库实例。

代理层方案的优点是:

  • 应用基本无感知,SQL 还是原来的 SQL;
  • 支持多语言接入,不绑定 .NET 技术栈。

缺点也很明显:

  • 引入独立部署组件,维护成本变高;
  • SQL 解析层会带来额外延迟;
  • 复杂 SQL 兼容性有限。

3.3 自研数据访问层

如果业务场景比较简单、分片规则非常固定,也可以自己在仓储层做分片路由。比如封装一个OrderRepository,写入时对订单号取模选择表名,查询时按用户 ID 路由。

自研方案的优点是灵活、轻量、没有黑盒,缺点是从零实现要考虑到分页、事务、扩容、跨分片聚合等大量问题。如果团队对分片规则非常有把握,而且业务不会频繁变化,这种做法可控性反而最高。

从我的判断来看:中小团队和业务快速迭代的项目,优先考虑基于 ORM 的中间件;大型团队、多个技术栈并存时,代理层更合适;分片规则稳定且简单的系统,自研也没有问题。

4. 分片键设计:三种常见分片策略与实现

分片键是分库分表的核心。分片键选得好不好,直接决定系统的扩展能力和查询效率。

分片键设计总的原则是:让大多数查询能直接命中某一个分片,避免全分片扫描。

4.1 哈希取模分片

这是最常用的策略。选择业务上分布均匀的字段作为分片键,比如用户 ID、订单号。对分片键计算哈希值,然后对分片数量取模,得到目标分片号。

哈希取模的优点是数据分布均匀,缺点是扩容时数据迁移量大。原来 4 个分片扩到 8 个分片,几乎所有数据都可能要重排,这时通常配合一致性哈希来解决。

下面是一个可以放到工具类里的取模路由逻辑,用 MD5 计算稳定哈希值,避免依赖运行时的GetHashCode在不同进程间结果不一致的问题:

// 文件路径:Sharding/ShardingStrategy.cs using System.Security.Cryptography; using System.Text; public static class ShardingStrategy { /// <summary> /// 基于稳定哈希的分片路由 /// </summary> /// <param name="shardingKey">分片键,如用户ID</param> /// <param name="shardCount">分片总数</param> /// <returns>分片索引,从 0 开始</returns> public static int HashModShard(string shardingKey, int shardCount) { using var md5 = MD5.Create(); var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(shardingKey)); var value = BitConverter.ToUInt64(hashBytes, 0); return (int)(value % (ulong)shardCount); } public static int HashModShard(long shardingKey, int shardCount) { return HashModShard(shardingKey.ToString(), shardCount); } }

这段代码的核心是用 MD5 把字符串分片键转换成高散列的数值,再对分片总数取模。使用场景是:分片键类型不确定,或者需要兼容字符串和数值类型时,统一走字符串重载即可。

4.2 范围分片

按时间范围或 ID 范围分片,比如orders_202401orders_202402,每个月一张表。这种策略实现简单,扩容方便,非常适合日志、流水类数据,但需要处理热点问题:当月数据总是集中在最新一张表。

范围分片的建表思路是在写入时根据业务时间拼接表名:

// 文件路径:Sharding/TableNameBuilder.cs public static class TableNameBuilder { public static string BuildByMonth(DateTime time, string tablePrefix) { return $"{tablePrefix}_{time:yyyyMM}"; } }

使用方式示例:

var tableName = TableNameBuilder.BuildByMonth(DateTime.Now, "orders"); // 输出:orders_202512

范围分片和哈希取模可以组合使用。常见做法是:先按用户 ID 取模分库,再按时间分表。这样既解决了单用户数据量大时的查询效率问题,也解决了单库热点问题。

4.3 映射表分片

如果分片键无法直接从业务数据中确定,或者业务上需要通过多个字段来定位,可以加一张“分片映射表”,记录业务主键和分片编号的关系。

这种方式最灵活,但会额外引入一次查询开销,而且映射表本身也可能变成瓶颈。实际项目中,映射表通常配合缓存使用,减少数据库查询次数。

4.4 分片键选型建议

分片键不要选“选择起来很别扭”的字段,也不要只考虑写入场景。下面几条原则比较实用:

  • 优先选择查询频率最高的字段,比如用户 ID;
  • 优先选择值分布均匀的字段,避免订单状态这种只有几个取值的字段;
  • 优先选择不可变字段,如果业务主键本身会变,分片键也会跟着变,数据迁移成本很高;
  • 秒杀、抢购这类写热点极高的系统,要考虑按商品拆,而不是按用户拆。

5. 用 AI 辅助生成分库分表方案的思路

标题里提到 AI 辅助落地,这里展开说一下。AI 辅助分库分表不是一个“一键完成”的黑魔法,它更像一个快速生成草稿和扫描盲区的工具。用得好,能省掉大量写模板代码的时间,但最终决定权仍然在架构师手里。

5.1 AI 能帮忙做的四件事

第一,生成分片算法原型。把分片需求描述清楚,AI 可以根据取模、一致性哈希、时间范围等策略,快速生成一段可运行的 C# 代码,比手写再改 bug 要快很多。

第二,分析现有 SQL 和 LINQ 查询在分片后的兼容性。把查询语句贴给 AI,让它判断哪些字段会成为分片键、哪些查询会变成跨分片查询,并给出改写建议。

第三,生成数据迁移和校验脚本。分库分表上线前要把存量数据从单表迁移到多个分片,AI 可以帮你生成迁移脚本、校验脚本、异常数据对账脚本。

第四,生成幂等的初始化脚本。分片表结构需要批量创建,AI 可以帮你生成循环建表 SQL。

5.2 一个可复用的提示词模板

下面这个提示词模板比较通用,可以收藏:

我有一张订单表 orders,目前单表数据量约 5000 万行,后续仍在快速增长。 订单表主要通过 user_id 查询用户订单,也支持按 order_no 查询单条订单。 请帮我设计一个水平分库分表方案,并输出: 1. 分片键选择和分片策略建议; 2. 分库分表后的实体类和仓储层代码,使用 C# / EF Core; 3. 4 个分片的建表 SQL; 4. 使用 user_id 分页查询订单的示例代码; 5. 使用 order_no 精确查询时的处理方案。

注意,AI 生成的代码仍然需要人工 review。尤其是分片策略、事务边界、异常处理这些关键逻辑,不要让 AI 直接决定。

5.3 AI 辅助落地的正确姿势

我比较推荐的做法是:先自己理解分库分表的核心原理,再让 AI 帮忙加速。如果对分片路由、跨分片分页这些概念不熟,AI 生成的代码很难判断对不对。

AI 还有一个重要用途是生成完整的迁移检查清单。比如:

  • 存量数据有没有全部迁移;
  • 分片键和分片算法是否匹配;
  • 旧表是否还需要保留只读备份;
  • 灰度期间双写方案。

这些清单让 AI 生成初版,再用项目实际场景去校验,效率会高很多。

6. 基于 ShardingCore 的完整项目落地示例

下面进入实操环节。我们用一个订单查询接口作为例子,演示从创建 WebAPI 项目到配置分库分表、编写查询接口的完整过程。

6.1 创建 WebAPI 项目与安装依赖

首先创建一个 ASP.NET Core WebAPI 项目:

dotnet new webapi -n ShardingDemo cd ShardingDemo

然后添加 NuGet 依赖。这里以 ShardingCore 为例,对应 EF Core 版本请根据当前项目实际情况选择,不要盲目装最新版。

dotnet add package ShardingCore dotnet add package Microsoft.EntityFrameworkCore.SqlServer

如果使用 MySQL,把第二个包换成Pomelo.EntityFrameworkCore.MySql或者MySql.EntityFrameworkCore,以你的数据库类型为准。

6.2 配置多数据源

appsettings.json中配置多个库的数据源。这里我们拆成两个库,每个库里都有相同的表结构。

{ "ConnectionStrings": { "Default": "Server=localhost;Database=order_db;Uid=root;Pwd=123456;", "OrderDb0": "Server=localhost;Database=order_db_0;Uid=root;Pwd=123456;", "OrderDb1": "Server=localhost;Database=order_db_1;Uid=root;Pwd=123456;" }, "Sharding": { "ShardCount": 2 } }

这里ShardCount是自定义配置项,用来告诉代码分片总数。具体连接串按你自己的环境修改。

Program.cs中注册 DbContext 和 ShardingCore:

// 文件路径:Program.cs using Microsoft.EntityFrameworkCore; using ShardingCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // 注册 DbContext builder.Services.AddDbContext<OrderDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("Default"))); // 注册 ShardingCore,伪代码,具体 API 以当前版本官方文档为准 builder.Services.AddShardingDbContext<OrderDbContext>() .AddShardingTableConfigure(op => { // 配置分片实体和分片策略 }); var app = builder.Build(); app.UseAuthorization(); app.MapControllers(); // 启动时初始化分片表 using (var scope = app.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService<OrderDbContext>(); // 确保数据库已创建,生产环境请使用迁移脚本 context.Database.EnsureCreated(); } app.Run();

需要说明的是,ShardingCore 的 API 在不同版本之间有一些差异,上面AddShardingDbContext这种写法是框架早期的典型用法。在你落到自己系统里时,一定要以当前所用版本的官方 README 为准。

6.3 定义实体与分片映射

定义订单实体。这里我们选择UserId作为分片键,分表规则是按用户 ID 取模。

// 文件路径:Models/Order.cs using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; [Table("orders")] public class Order { [Key] public long Id { get; set; } public long UserId { get; set; } public string OrderNo { get; set; } public decimal Amount { get; set; } public DateTime CreateTime { get; set; } }

在 DbContext 中配置分片映射。这里的关键是告诉框架“这个实体对应的分片表名规则”,以及“分片键是哪个属性”。

// 文件路径:Data/OrderDbContext.cs using Microsoft.EntityFrameworkCore; public class OrderDbContext : DbContext { public OrderDbContext(DbContextOptions<OrderDbContext> options) : base(options) { } public DbSet<Order> Orders { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // order 实体按 UserId 分片,分片表命名规则为 orders_{0} modelBuilder.Entity<Order>() .HasShardingTable(o => o.UserId, "orders_{0}"); // 索引优化 modelBuilder.Entity<Order>() .HasIndex(o => new { o.UserId, o.CreateTime }); } }

HasShardingTable(o => o.UserId, "orders_{0}")这行的意思是:对UserId做分片计算,分片表名以orders_开头,后面接分片编号。实际表就是orders_0orders_1

6.4 编写查询接口

接下来写一个查询接口,支持按用户 ID 分页查询订单列表,以及按订单号查询单条订单。

// 文件路径:Controllers/OrderController.cs using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; [ApiController] [Route("api/orders")] public class OrderController : ControllerBase { private readonly OrderDbContext _dbContext; public OrderController(OrderDbContext dbContext) { _dbContext = dbContext; } /// <summary> /// 按用户 ID 分页查询订单 /// </summary> [HttpGet("user/{userId}")] public async Task<IActionResult> GetByUser(long userId, int pageIndex = 1, int pageSize = 20) { if (pageIndex < 1) pageIndex = 1; if (pageSize < 1 || pageSize > 100) pageSize = 20; var total = await _dbContext.Orders .Where(o => o.UserId == userId) .LongCountAsync(); var items = await _dbContext.Orders .Where(o => o.UserId == userId) .OrderByDescending(o => o.CreateTime) .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .ToListAsync(); return Ok(new { total, items }); } /// <summary> /// 按订单号查询,使用映射缓存或广播表方案 /// </summary> [HttpGet("no/{orderNo}")] public async Task<IActionResult> GetByOrderNo(string orderNo) { // 这里先演示逻辑,实际场景中如果有 user_id 字段一起传入,才能直接定位分片 var order = await _dbContext.Orders .FirstOrDefaultAsync(o => o.OrderNo == orderNo); if (order == null) { return NotFound(); } return Ok(order); } }

这里有两个非常重要的工程细节。

第一个是分页查询。在分库分表环境下,Skip/Take并不是简单地在单表上执行,而是会先在每个分片中分别取offset + pageSize行,然后合并排序,再在内存中做二次分页。这样实现逻辑是正确的,但当pageIndex非常大时,每个分片都要拿出大量数据到内存合并,性能会急剧恶化。应对思路是严格控制分页深度,禁止用户随便访问第 10000 页。

第二个是按订单号查询的问题。如果订单号不是分片键,直接查询会触发全分片扫描。如果业务上经常需要按订单号查询,建议维护一张order_no -> user_id的映射表,或者把用户 ID 冗余在订单号里。这部分内容在第八章里会展开讲。

6.5 启动项目与验证

启动项目:

dotnet run

访问接口,例如:

GET http://localhost:5000/api/orders/user/10001?pageIndex=1&pageSize=20

如果返回了数据库中的订单数据,说明通过分片路由查询成功。

如果你希望直接看到 SQL 路由到哪个分片,可以打开 EF Core 的日志。在appsettings.json中配置:

{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.EntityFrameworkCore.Database.Command": "Information" } } }

这样能在控制台看到生成的 SQL 和对应的表名。如果发现 SQL 查的是orders_0而不是全部分片,说明分片路由生效了。

7. 大数据量分页优化:跨分片分页的正确姿势

分页查询是分库分表后最容易被忽视的问题。单表分页时,OFFSET 100000, 20虽然慢,但往往还能接受;分库分表后,同样的分页逻辑会被放大 N 倍。

假设你有 4 个分片,要查第 5000 页,每页 20 条。那每个分片都要取出前 100000 条数据,到内存里排序后再取第 5000 页的 20 条。四个分片就相当于一次性处理了 40 万条数据,而且每次翻页都要重复这个过程。

解决方案通常有三种。

7.1 禁止深分页

最简单也最常用。前端只允许查看前 2000 条,超过 100 页的查询直接拒绝。这种方式适合普通后台管理页面。

7.2 游标分页

游标分页不计算偏移量,而是根据上次查询的最后一条记录来取下一页。比如按CreateTimeId排序,下一页的查询条件就是:

var items = await _dbContext.Orders .Where(o => o.UserId == userId && (o.CreateTime < lastCreateTime || (o.CreateTime == lastCreateTime && o.Id < lastId))) .OrderByDescending(o => o.CreateTime) .ThenByDescending(o => o.Id) .Take(pageSize) .ToListAsync();

游标分页的好处是每一页的数据量是固定的,不会随着页码增大而膨胀,性能非常稳定。缺点是前端只能点击“下一页”,不能直接跳转到第 N 页。

7.3 基于 Redis 缓存的总数和页码索引

对于不要求实时精确统计的场景,可以定期把分片内的数据分页索引同步到 Redis,用 ZSet 存储排序字段,分页直接在 Redis 中完成。这种方式适合榜单、热门列表这类读多写少的数据。

我的建议是:新系统首选游标分页,老系统尽量限制分页深度。深分页在分库分表环境里属于典型的“正确但很贵”的方案。

8. 联表查询与数据拆分合并实战

联表查询在单库时代非常简单,一个 join 就搞定。分库分表之后,如果两个表都不在同一个分片里,join 就会变成灾难。下面分三种情况说明。

8.1 第一种:广播表/字典表

用户表、商品表、地区表这类数据量不大、变动不频繁的表,可以冗余到每一个分片库里。这样订单表在本地分片内直接 join 商品表、用户表,不需要跨库查询。

这种表在中间件里通常叫“广播表”或“字典表”。实现上就是在每一个分库中创建相同结构的表,写入时同步到所有分库,读取时只访问本分片。

8.2 第二种:同分片字段冗余

如果订单表需要关联用户表,而用户表又无法做成广播表,可以考虑把用户表中订单查询常用的字段冗余到订单表里,比如user_nameuser_mobile。这样查询时不需要 join 用户表,直接从订单表取字段。

这就是典型的“反范式”设计。分库分表之后,数据库层的 join 能力被削弱,应用层和存储层要通过冗余字段换取查询效率。

8.3 第三种:应用层内存聚合

如果订单要关联商品,商品数量很大没办法做广播表,订单表里也没有商品的其他冗余字段,那就只能先把两边数据都查出来,在应用内存中做合并。

// 文件路径:Services/OrderAggregateService.cs public class OrderAggregateService { private readonly OrderDbContext _dbContext; private readonly IProductService _productService; public OrderAggregateService(OrderDbContext dbContext, IProductService productService) { _dbContext = dbContext; _productService = productService; } public async Task<List<OrderDetail>> GetOrderDetailsAsync(long userId, int pageIndex, int pageSize) { // 第一步:查询订单 var orders = await _dbContext.Orders .Where(o => o.UserId == userId) .OrderByDescending(o => o.CreateTime) .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .ToListAsync(); if (orders.Count == 0) { return new List<OrderDetail>(); } // 第二步:获取商品 ID 集合 var productIds = orders .Where(o => o.ProductId.HasValue) .Select(o => o.ProductId.Value) .Distinct() .ToList(); // 第三步:调用商品服务批量查询 var products = await _productService.GetByIdsAsync(productIds); // 第四步:在内存中合并订单与商品信息 var detailList = orders.Select(order => { var product = products.FirstOrDefault(p => p.Id == order.ProductId); return new OrderDetail { OrderId = order.Id, OrderNo = order.OrderNo, ProductName = product?.Name, ProductPrice = product?.Price, CreateTime = order.CreateTime }; }).ToList(); return detailList; } }

这个示例的关键点是:分库分表环境下,跨分片的关联查询尽量拆成“多次查询 + 内存合并”,而不是试图写一条跨分片 join 让框架硬撑。

应用层聚合虽然多了一步网络调用或内存操作,但好处是每个查询都可控,可以针对每类数据分别做缓存、做分页、做降级,比让数据库做跨分片 join 稳定得多。

9. 常见问题与排查思路

分库分表落地过程中,我见过很多团队踩在相同的坑上。下面整理成表格,方便排查。

问题现象可能原因排查方式解决方案
查询结果不全分片键判断失误,部分数据路由到错误分片检查分片算法,对比分片前后的数据量修正分片路由规则,重新迁移数据
每次查询都很慢查询未携带分片键,触发了全分片扫描查看 SQL 日志,确认是否路由到单分片根据业务加入分片键条件,或维护映射表
深分页触发 OOM跨分片Skip/Take拉取大量数据到内存检查分页深度和分片数量改用游标分页或限制分页深度
事务失效一个事务涉及多个分片,中间件不支持跨分片事务检查事务上下文和报错日志按分片键设计聚合根,避免跨分片事务
建表 SQL 重复执行报错迁移脚本在多个分片重复执行检查迁移脚本日志增加幂等判断,比如IF NOT EXISTS
接入中间件后 LINQ 查询报错部分查询语法中间件不支持查看异常堆栈和解析日志改写查询,去掉不支持的语法,分步查询
扩容后部分数据查不到取模分母变化,重新哈希后路由失效对比新老路由结果双写迁移,或使用一致性哈希

这里最容易翻车的是第一条和第七条。哈希取模看起来很直观,但扩容时冷数据不会自动跟随新的取模结果移动。一旦你把分片数从 4 改成 8,老数据还留在原来的分片,新查询按 8 取模却去新分片找,结果就是“数据消失了”。

这类问题的系统化解法是预留足够分片数,或者一开始就使用一致性哈希。如果已经上线了,就只能做数据迁移,不能简单改配置。

10. 最佳实践与工程建议

分库分表项目里,工程规范比代码本身更重要。下面这几条建议来自实际项目教训,值得写进团队规范。

10.1 分片键必须进入所有核心查询

在代码评审时,我会重点检查每一个订单查询接口,看它的查询条件里是否携带了分片键。如果没有,要么是功能设计有问题,要么是需要走映射表。设计接口时,尽量通过路径参数、Token、Header 传递当前用户 ID,保证分片键可用。

10.2 所有分片表结构变更要脚本化管理

分片表不是一张表,是 N 张表。任何ALTER TABLE都不能靠手工在单个库执行,而要通过脚本批量执行,并且脚本要具备幂等性。

-- 示例:对所有分片订单表增加字段 -- 生产环境请先备份,再在测试环境验证 ALTER TABLE orders_0 ADD COLUMN buyer_remark VARCHAR(200) NULL; ALTER TABLE orders_1 ADD COLUMN buyer_remark VARCHAR(200) NULL;

更推荐的方式是用迁移工具管理,而不是手写脚本。跑完迁移后,要检查每个分片的表结构是否一致。

10.3 日志里必须打印分片路由信息

排查分库分表问题时,最头疼的就是不知道一条 SQL 到底落在了哪个分片。建议在开发环境开启 SQL 日志,在生产环境至少记录慢查询日志,并打印分片表名。

10.4 监控和告警要覆盖分片维度

不要只监控整个数据库集群的总连接数和总 QPS,要看每个分片库的负载是否均衡。如果发现某个分片的数据量明显偏大,就要检查分片键的取模分布。

10.5 上线前必须演练数据迁移回滚

分库分表上线不是“跑一次迁移脚本”就结束了。要提前准备回滚方案,比如保留旧表的只读备份、记录迁移断点、做数据对账。一般会通过双写的方式灰度:先把新数据同时写到单表和新分片,确认稳定后再把旧数据批量迁移,最后切换读流量。

11. 结束语:下一步该怎么走

分库分表的核心不是把表拆开,而是拆开之后,系统依然能正确、高效地完成数据读写。它真正考验的是架构师对分片键、查询模式、数据分布和数据迁移的掌控力。

如果你是刚开始接触这个方向的 .NET 开发者,下一步可以这么做:

  • 先用一个简单的 WebAPI 项目,实现按用户 ID 取模的路由逻辑,把数据写入不同的表;
  • 再尝试在中间件环境里配置多数据源,跑通分页查询和联表查询;
  • 然后写一套基于映射表的分片方案,对比不同查询场景下的性能差异;
  • 最后再考虑数据迁移、双写、灰度发布这些工程化内容。

数据库没有万能的架构,分库分表也不是所有系统都必须经历的一步。很多业务通过合理的索引、缓存、读写分离就能支撑到足够大的规模。只有当单表数据量和写入吞吐成为确定性瓶颈时,再启动分库分表改造,才是最稳妥的节奏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 14:00:34

SpringBoot整合MQTT:物联网设备接入与报文解析实战

去年我接手一个环境监测平台的后端改造&#xff0c;硬件端是几十台温湿度、空气质量采集网关&#xff0c;每台网关下面挂着若干传感器节点。旧方案是设备通过HTTP POST每30秒上报一次JSON&#xff0c;后端用SpringBoot接口接收再写入MySQL。设备规模到两百台左右时问题集中爆发…

作者头像 李华
网站建设 2026/9/7 14:00:23

让AI Coding Agent真正懂你:规则文件与记忆库构建实战

同一个Agent&#xff0c;为什么在别人手里像并肩作战多年的搭子&#xff0c;到你手里就成了一个记性差得要命的新实习生&#xff1f;这是我在好几个团队里反复观察到的问题。很多人以为AI Coding Agent的能力差距来自于模型本身&#xff0c;其实大部分时候&#xff0c;瓶颈出在…

作者头像 李华
网站建设 2026/9/7 13:54:19

萌妹之路2:求生之路2萌系MOD整合版试玩与优化指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 13:53:19

嵌入式面试全复盘:MCU/Linux考点与项目实战经验

今年我完整跑了一轮嵌入式岗位的面试流程&#xff0c;十来家公司&#xff0c;覆盖MCU固件、嵌入式Linux应用、Linux驱动和一小部分边缘AI方向。复盘下来有个特别强烈的感受&#xff1a;嵌入式面试考察范围看起来无边无际&#xff0c;从C语言八股文到硬件协议再到项目深挖全都考…

作者头像 李华
网站建设 2026/9/7 13:53:01

MODIS NPP长时间序列栅格处理全流程:从预处理到趋势分析

简介&#xff1a;面向生态遥感和GIS分析人员&#xff0c;这份数据包汇集了2005—2021年中国西北地区&#xff08;新疆、青海、甘肃、内蒙古和宁夏&#xff09;1000米分辨率的年际NPP栅格数据&#xff0c;源自MODIS MOD17A3HGF产品并重采样生成&#xff0c;单位为g*C/m^2&#x…

作者头像 李华
网站建设 2026/9/7 13:52:24

AI绘画多人物比例控制:从ControlNet到提示词工程的完整解决方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华