十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

.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_0、orders_1、orders_2、orders_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 nameshardingKey分片键如用户ID/param /// param nameshardCount分片总数/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_202401、orders_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: Serverlocalhost;Databaseorder_db;Uidroot;Pwd123456;, OrderDb0: Serverlocalhost;Databaseorder_db_0;Uidroot;Pwd123456;, OrderDb1: Serverlocalhost;Databaseorder_db_1;Uidroot;Pwd123456; }, 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.AddDbContextOrderDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(Default))); // 注册 ShardingCore伪代码具体 API 以当前版本官方文档为准 builder.Services.AddShardingDbContextOrderDbContext() .AddShardingTableConfigure(op { // 配置分片实体和分片策略 }); var app builder.Build(); app.UseAuthorization(); app.MapControllers(); // 启动时初始化分片表 using (var scope app.Services.CreateScope()) { var context scope.ServiceProvider.GetRequiredServiceOrderDbContext(); // 确保数据库已创建生产环境请使用迁移脚本 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(DbContextOptionsOrderDbContext options) : base(options) { } public DbSetOrder Orders { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // order 实体按 UserId 分片分片表命名规则为 orders_{0} modelBuilder.EntityOrder() .HasShardingTable(o o.UserId, orders_{0}); // 索引优化 modelBuilder.EntityOrder() .HasIndex(o new { o.UserId, o.CreateTime }); } }HasShardingTable(o o.UserId, orders_{0})这行的意思是对UserId做分片计算分片表名以orders_开头后面接分片编号。实际表就是orders_0、orders_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 TaskIActionResult 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 TaskIActionResult 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?pageIndex1pageSize20如果返回了数据库中的订单数据说明通过分片路由查询成功。如果你希望直接看到 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 游标分页游标分页不计算偏移量而是根据上次查询的最后一条记录来取下一页。比如按CreateTime和Id排序下一页的查询条件就是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_name、user_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 TaskListOrderDetail 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 ListOrderDetail(); } // 第二步获取商品 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 取模的路由逻辑把数据写入不同的表再尝试在中间件环境里配置多数据源跑通分页查询和联表查询然后写一套基于映射表的分片方案对比不同查询场景下的性能差异最后再考虑数据迁移、双写、灰度发布这些工程化内容。数据库没有万能的架构分库分表也不是所有系统都必须经历的一步。很多业务通过合理的索引、缓存、读写分离就能支撑到足够大的规模。只有当单表数据量和写入吞吐成为确定性瓶颈时再启动分库分表改造才是最稳妥的节奏。
返回列表