ARTICLE DETAIL

资讯详情

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

.NET WebAPI高并发场景下的分库分表实战与难点解析

.NET WebAPI高并发场景下的分库分表实战与难点解析 做 .NET 后端开发的同学可能都有过这样的经历接口在测试环境跑得好好的一到生产环境随着单表数据量突破百万、千万级SQL 越来越慢数据库连接数经常被打满接口超时和报警轮着来。加索引、加缓存、上读写分离这些都是常规优化手段但当数据增长速度远超预期时分库分表就会变成一个绕不开的话题。本文围绕.NET WebAPI 高并发场景从概念讲起逐步拆解分库分表的设计思路和落地过程重点覆盖三个最难处理的场景大数据量分页、跨分片联表查询、数据拆分与合并。同时会分享 AI 辅助编码工具在分库分表落地过程中能帮上什么忙、不能帮什么忙。内容偏实战代码会尽量完整你可以直接照着搭一个最小可运行的 .NET WebAPI 分库分表示例。1. 分库分表是什么、什么时候该上1.1 先用一句话说清楚分库分表本质上就是把原本放在一个数据库、一张表里的数据按照某种规则分散到多个数据库、多张表中去存储。分库解决的是“单库性能瓶颈”比如连接数有限、磁盘 IO 压力大、主从同步延迟等问题。分表解决的是“单表数据量过大”比如千万级、亿级数据下即使走了索引B 树层级变高查询性能也会明显下降。举一个最容易理解的例子假设你有一个订单表每天新增 10 万条订单数据。一年下来就是 3600 万条两年就是 7000 多万条。这时候你会发现订单详情查询偶尔会慢。按用户查订单列表深分页时特别慢。月底跑报表直接把数据库 CPU 打满。大表加字段、加索引耗时很长且容易造成锁表。通过分库分表把订单数据按照某个维度拆到 4 个库、每库 4 张表里单表数据量就会从 7000 万降到 400 多万查询性能和组织结构都会舒服很多。1.2 什么时候不需要分库分表很多项目在数据量还没起来之前就急着上分库分表结果是系统复杂度成倍上升收益却没多少。如果你的数据量在几百万行以内、单库连接数不紧张、读写延迟在可接受范围内优先考虑合理设计索引。热数据缓存。读写分离。冷热数据归档。SQL 本身优化。分库分表是最后的兜底手段不是第一选择。1.3 分库分表之后系统会有哪些变化这是个需要提前有心理预期的点。分库分表不是单纯把表拆开就结束了它会给业务开发带来一系列限制影响点说明跨库 join 受限不同库之间的表不能直接 join需要通过冗余字段、应用层合并或汇总表解决分布式事务复杂跨库事务不能依赖本地事务需要引入可靠消息、Saga 等方案分页查询变复杂单表 limit 变成多分片聚合深度分页需要重新设计主键不能自增自增 ID 在分片下会重复需要引入雪花 ID、号段模式等分布式 ID 方案数据迁移成本高存量数据需要从单表迁移到分片表需要设计迁移和校验方案这也是为什么我建议先想清楚再动手。2. 环境准备与方案选型2.1 本文环境说明示例代码以 Windows Visual Studio 2022 环境为主你使用 VS Code、Rider 也可以。需要用到的技术栈.NET 8 SDK 或更高版本示例基于 ASP.NET Core WebAPI。Visual Studio 2022社区版即可。MySQL 8.x也可以替换为 SQL ServerSQL 语法差异不大。Dapper用于轻量级数据库访问便于展示分片路由思路。Postman / Apifox用于接口调用测试。版本不需要完全一致重点是思路。实际项目中如果你用的是 .NET 6 / .NET 7代码同样可以迁移。2.2 框架选型自研路由还是成熟框架在 .NET 生态里分库分表中间件不像 Java 那边那么丰富。常见的选择大概有三种方案优点缺点ShardingCore支持 EF Core 和多种数据库分页、聚合、路由封装较好版本迭代较快需要关注兼容性数据库中间件如 MyCat、ShardingSphere对应用透明Java 生态成熟与 .NET 配合时需要额外考虑协议兼容和运维成本自研分片路由轻量、可控、按需实现业务贴合度高需要自己处理分页、聚合、迁移等复杂逻辑如果你的项目团队人力有限且希望快速见效可以调研一下 ShardingCore如果你想要最大程度地掌控路由逻辑自研路由层是很好的选择。本文为了把原理讲透采用自研分片路由的方式来演示核心代码量并不大而且你会很清楚每一步在做什么。2.3 示例项目结构规划我们以一个订单系统为例假设订单表要拆成 2 个库、每库 4 张表OrderService ├── Controllers │ └── OrderController.cs ├── Models │ └── Order.cs ├── Repositories │ ├── IOrderRepository.cs │ └── OrderRepository.cs ├── Sharding │ ├── ShardRouter.cs │ └── DbConnectionFactory.cs ├── Services │ └── OrderService.cs ├── appsettings.json └── Program.cs最终要实现的接口包括创建订单。根据订单 ID 查询订单。按用户 ID 分页查询订单。按订单 ID 联表查询订单明细。存量数据从单表迁移到分片表。跨分片汇总统计。3. 核心设计分片键、路由算法与数据架构3.1 分片键怎么选分片键是分库分表里最重要的决策没有之一。分片键决定了数据按照什么维度分布直接影响查询效率和扩展性。订单场景里常见选择是按order_id分片适合按订单查询但按用户查询时需要广播到所有分片。按user_id分片适合按用户查询但按订单号查询时也面临同样的问题。按order_time分片适合时间范围查询但可能产生数据热点。没有完美的分片键关键是看你的核心查询路径是什么。本文示例以order_id为主分片键因为订单详情查询是最高频的操作。针对按用户分页查询我们会在后续专门处理跨分片聚合问题。3.2 路由算法哈希取模最简单的路由算法是取模// 文件路径Sharding/ShardRouter.cs using System.Security.Cryptography; using System.Text; namespace OrderService.Sharding; public static class ShardRouter { private const int DbCount 2; private const int TableCountPerDb 4; /// summary /// 根据订单号计算路由结果 /// /summary public static (string DbName, string TableName) GetRoute(long orderId) { // 计算完整性先用哈希散列再取模避免连续订单号扎堆 var hash BitConverter.ToInt32(MD5.HashData(Encoding.UTF8.GetBytes(orderId.ToString()))); var total DbCount * TableCountPerDb; var index Math.Abs(hash) % total; var dbIndex index / TableCountPerDb; var tableIndex index % TableCountPerDb; return ($order_db_{dbIndex}, $t_order_{tableIndex}); } /// summary /// 用于数据迁移和广播查询时获取全部分片表名 /// /summary public static List(string DbName, string TableName) GetAllRoutes() { var routes new List(string, string)(); for (var db 0; db DbCount; db) { for (var table 0; table TableCountPerDb; table) { routes.Add(($order_db_{db}, $t_order_{table})); } } return routes; } }这里解释几个关键点取模基数固定为DbCount * TableCountPerDb也就是 8。这样库和表通过同一个 index 定位保证数据分布均匀。使用MD5哈希后取模而不是直接用orderId % 8是为了避免连续订单号序列导致数据倾斜。GetAllRoutes()方法用于广播查询和存量迁移后面会用到。需要注意生产环境分片数量基本确定后不建议再频繁变更。如果后续要扩容一般的做法是2 倍扩容并配合一致性哈希或双写迁移方案。3.3 连接管理因为在两个库之间切换需要按库名维护不同的连接字符串。这里用DbConnectionFactory根据库名返回连接。// 文件路径Sharding/DbConnectionFactory.cs using MySqlConnector; namespace OrderService.Sharding; public class DbConnectionFactory { private readonly Dictionarystring, string _connectionStrings; public DbConnectionFactory(Dictionarystring, string connectionStrings) { _connectionStrings connectionStrings; } public MySqlConnection CreateConnection(string dbName) { if (!_connectionStrings.TryGetValue(dbName, out var connectionString)) { throw new ArgumentException($未找到数据库 {dbName} 的连接配置); } return new MySqlConnection(connectionString); } }在appsettings.json中连接串可以平铺配置{ ConnectionStrings: { order_db_0: Serverlocalhost;Port3306;Databaseorder_db_0;Uidroot;Pwd123456;, order_db_1: Serverlocalhost;Port3306;Databaseorder_db_1;Uidroot;Pwd123456; } }真实项目中建议把连接串放到配置中心或环境变量里避免写死在代码仓库中。4. 完整实战.NET WebAPI 订单系统分库分表4.1 创建项目在 Visual Studio 2022 中依次选择“新建项目” - “ASP.NET Core Web API”项目名称填写OrderService。创建完成后通过 NuGet 安装以下包dotnet add package Dapper dotnet add package MySqlConnector如果你使用的是 SQL Server可以改为安装Microsoft.Data.SqlClientSQL 语法需要做少量调整。4.2 编写 Program.cs// 文件路径Program.cs using OrderService.Repositories; using OrderService.Sharding; var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // 注册连接工厂 var connectionStrings builder.Configuration .GetSection(ConnectionStrings) .GetChildren() .ToDictionary(x x.Key, x x.Value!); builder.Services.AddSingleton(new DbConnectionFactory(connectionStrings)); // 注册仓储层 builder.Services.AddScopedIOrderRepository, OrderRepository(); var app builder.Build(); app.MapControllers(); app.Run();4.3 订单实体定义// 文件路径Models/Order.cs namespace OrderService.Models; public class Order { public long Id { get; set; } public long UserId { get; set; } public string ProductName { get; set; } string.Empty; public decimal Amount { get; set; } public int Status { get; set; } public DateTime CreateTime { get; set; } }说明为了演示方便订单明细单独放在t_order_item_{tableIndex}表这条表在后续联表查询部分会用到。4.4 仓储层插入与单点查询// 文件路径Repositories/IOrderRepository.cs using OrderService.Models; namespace OrderService.Repositories; public interface IOrderRepository { Taskint InsertAsync(Order order); TaskOrder? GetByIdAsync(long orderId); TaskListOrder GetOrdersByUserIdAsync(long userId, int pageIndex, int pageSize); TaskListOrder GetAllOrdersAsync(); TaskListOrder GetOrdersWithItemsByOrderIdAsync(long orderId); }下面是最核心的分片读写实现// 文件路径Repositories/OrderRepository.cs using Dapper; using OrderService.Models; using OrderService.Sharding; namespace OrderService.Repositories; public class OrderRepository : IOrderRepository { private readonly DbConnectionFactory _connectionFactory; public OrderRepository(DbConnectionFactory connectionFactory) { _connectionFactory connectionFactory; } public async Taskint InsertAsync(Order order) { var (dbName, tableName) ShardRouter.GetRoute(order.Id); const string sql INSERT INTO {0} (Id, UserId, ProductName, Amount, Status, CreateTime) VALUES (Id, UserId, ProductName, Amount, Status, CreateTime); ; await using var connection _connectionFactory.CreateConnection(dbName); return await connection.ExecuteAsync(string.Format(sql, tableName), order); } public async TaskOrder? GetByIdAsync(long orderId) { var (dbName, tableName) ShardRouter.GetRoute(orderId); const string sql SELECT Id, UserId, ProductName, Amount, Status, CreateTime FROM {0} WHERE Id Id LIMIT 1; ; await using var connection _connectionFactory.CreateConnection(dbName); return await connection.QueryFirstOrDefaultAsyncOrder( string.Format(sql, tableName), new { Id orderId }); } public async TaskListOrder GetOrdersByUserIdAsync(long userId, int pageIndex, int pageSize) { var routes ShardRouter.GetAllRoutes(); var tasks routes.Select(async route { await using var connection _connectionFactory.CreateConnection(route.DbName); var sql $ SELECT Id, UserId, ProductName, Amount, Status, CreateTime FROM {route.TableName} WHERE UserId UserId ORDER BY CreateTime DESC LIMIT Limit OFFSET Offset; ; var offset (pageIndex - 1) * pageSize; var result await connection.QueryAsyncOrder(sql, new { UserId userId, Limit pageSize, Offset offset }); return result.AsList(); }); var allPages await Task.WhenAll(tasks); // 内存中合并排序取出目标页数据 return allPages .SelectMany(x x) .OrderByDescending(x x.CreateTime) .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .ToList(); } public async TaskListOrder GetAllOrdersAsync() { var routes ShardRouter.GetAllRoutes(); var result new ListOrder(); foreach (var route in routes) { await using var connection _connectionFactory.CreateConnection(route.DbName); var sql $ SELECT Id, UserId, ProductName, Amount, Status, CreateTime FROM {route.TableName}; ; var orders await connection.QueryAsyncOrder(sql); result.AddRange(orders); } return result; } public async TaskListOrder GetOrdersWithItemsByOrderIdAsync(long orderId) { var (dbName, tableName) ShardRouter.GetRoute(orderId); var itemTableName tableName.Replace(t_order_, t_order_item_); var sql $ SELECT o.Id, o.UserId, o.ProductName, o.Amount, o.Status, o.CreateTime, i.ItemName, i.Price FROM {tableName} o INNER JOIN {itemTableName} i ON o.Id i.OrderId WHERE o.Id OrderId; ; await using var connection _connectionFactory.CreateConnection(dbName); var rows await connection.QueryAsyncOrderItemView(sql, new { OrderId orderId }); return rows.Select(r new Order { Id r.Id, UserId r.UserId, ProductName r.ProductName, Amount r.Amount, Status r.Status, CreateTime r.CreateTime }).ToList(); } }这段代码有几点需要特别说明表名拼接问题tableName来自ShardRouter内部生成的白名单不是用户输入所以这里用字符串格式化是安全的。但如果你有一天改成从外部传入表名必须增加白名单校验否则会引入 SQL 注入风险。按用户分页查询因为数据分散在 8 张表里无法只查一张表所以采用广播查询 内存合并的方式。这只是能跑通的简单版并不适合大数据量场景后面会进一步优化。联表查询核心思路是“订单表”和“订单明细表”使用同样的分片键通过orderId路由后两张表一定在同一库、同一分片内因此可以正常 join。这就是同分片键设计的意义。4.5 控制器接口// 文件路径Controllers/OrderController.cs using Microsoft.AspNetCore.Mvc; using OrderService.Models; using OrderService.Repositories; namespace OrderService.Controllers; [ApiController] [Route(api/[controller])] public class OrderController : ControllerBase { private readonly IOrderRepository _orderRepository; public OrderController(IOrderRepository orderRepository) { _orderRepository orderRepository; } [HttpPost] public async TaskIActionResult Create([FromBody] Order order) { var result await _orderRepository.InsertAsync(order); return Ok(new { success result 0 }); } [HttpGet({orderId})] public async TaskIActionResult GetById(long orderId) { var order await _orderRepository.GetByIdAsync(orderId); return order is null ? NotFound() : Ok(order); } [HttpGet(user/{userId})] public async TaskIActionResult GetByUser(long userId, int pageIndex 1, int pageSize 10) { var orders await _orderRepository.GetOrdersByUserIdAsync(userId, pageIndex, pageSize); return Ok(orders); } [HttpGet(with-items/{orderId})] public async TaskIActionResult GetWithItems(long orderId) { var orders await _orderRepository.GetOrdersWithItemsByOrderIdAsync(orderId); return Ok(orders); } }4.6 初始化两个数据库为了能跑通示例需要提前创建两个数据库并分别建表。-- 数据库 order_db_0、order_db_1 中分别执行 CREATE TABLE t_order_0 ( Id bigint NOT NULL, UserId bigint NOT NULL, ProductName varchar(128) NOT NULL, Amount decimal(18,2) NOT NULL, Status int NOT NULL, CreateTime datetime NOT NULL, PRIMARY KEY (Id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 依次创建 t_order_1 ... t_order_7 -- 订单明细表 t_order_item_0 ... t_order_item_7 CREATE TABLE t_order_item_0 ( Id bigint NOT NULL, OrderId bigint NOT NULL, ItemName varchar(128) NOT NULL, Price decimal(18,2) NOT NULL, PRIMARY KEY (Id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意生产环境建表建议写成脚本文件并纳入版本管理不要手动在数据库客户端里一次次执行。启动项目后用 Postman 调用POST /api/order { id: 1000001, userId: 20001, productName: 手机, amount: 3999.00, status: 1, createTime: 2025-01-01 10:00:00 }响应为{ success: true }然后查询GET /api/order/1000001返回对应的订单数据说明订单被成功写入到某个分片并且能按分片路由查询到。5. 三大难点场景分页、联表、拆分合并上面的例子能让你理解分库分表的基本工作方式但把所有问题都暴露出来了。特别是分页、联表查询和存量数据拆分这几个场景是分库分表落地时最容易翻车的地方。5.1 大数据量分页为什么难应该怎么办前面代码里GetOrdersByUserIdAsync使用了最直观的“广播查询 内存合并”。这在数据量不大时没有问题但当分片数量很多、每个分片数据量都很大时问题很明显每个分片都执行了一次ORDER BY CreateTime DESC LIMIT ? OFFSET ?。所有分片结果都回传到应用层内存占用高。深度分页时比如查第 10000 页每个分片都要查到第 10000 页的数据性能极差。针对不同业务场景推荐以下三种方案方案一禁止深度分页改为游标分页保留CreateTime作为排序字段用上次最后一条记录的 CreateTime作为查询条件。SELECT Id, UserId, ProductName, Amount, Status, CreateTime FROM t_order_0 WHERE UserId UserId AND CreateTime LastCreateTime ORDER BY CreateTime DESC LIMIT PageSize;游标分页的优点是在数据量增长后依旧稳定缺点是只能一页一页往下翻不能直接跳页。对于订单列表、消息列表这类场景游标分页完全够用。方案二使用汇总表 / 索引表如果“某个用户的订单列表”查询频率很高可以单独建一张user_order_index表这张表按user_id分片存储用户最近订单的摘要信息。主表按order_id分片用于详情查询索引表按user_id分片用于列表查询。这本质上是空间换时间。方案三关键词搜索类场景交给搜索引擎如果分页条件特别多包含商品名、状态、时间范围等就不太适合直接在分片表上聚合。建议引入 Elasticsearch 之类的搜索引擎将分片表数据同步到 ES由 ES 承担复杂检索和分页拿到匹配的订单 ID 后再回查订单分片表获取详情。5.2 联表查询的正确打开方式很多人第一次接触分库分表时最不适应的地方就是不能随意 join 了。记住一句话跨分片 join 是反模式能避免就避免。常见的替代思路有四种反范式设计在订单表里冗余一份商品名称、商品快照查询订单时不需要再关联商品表。这是最推荐的做法。同分片键设计订单表和订单明细表都使用order_id作为分片键。这样按订单 ID 查询时两张表落在同一库的同一分片可以正常 join。前面代码演示的就是这个方式。应用层拼接先查主表再根据关联 ID 批量查从表在应用层完成数据组装。汇总表/宽表对于需要多表聚合的统计场景另外维护一张冗余的宽表。实际项目中第一种和第二种配合使用是最常见的。不要追求 SQL 写法上的优雅分布式环境下“能跑且可控”才是第一优先级。5.3 存量数据拆分与合并分库分表方案上线之前通常已经有一张装满历史数据的单表。我们不能直接删库跑路需要把存量数据平滑迁移到分片表中。这里给出一个简化的迁移思路// 文件路径Services/OrderMigrationService.cs using Dapper; using OrderService.Models; using OrderService.Sharding; namespace OrderService.Services; public class OrderMigrationService { private readonly DbConnectionFactory _connectionFactory; private readonly string _sourceConnectionString; public OrderMigrationService(DbConnectionFactory connectionFactory, string sourceConnectionString) { _connectionFactory connectionFactory; _sourceConnectionString sourceConnectionString; } public async Taskint MigrateAsync(long batchSize 1000) { var total 0; var lastId 0L; // 使用游标方式分批读取旧表避免一次加载全表导致内存溢出 while (true) { ListOrder batch; await using (var sourceConnection _connectionFactory.CreateConnection(source)) { const string sql SELECT Id, UserId, ProductName, Amount, Status, CreateTime FROM t_order WHERE Id LastId ORDER BY Id LIMIT BatchSize; ; batch (await sourceConnection.QueryAsyncOrder(sql, new { LastId lastId, BatchSize batchSize })).AsList(); } if (batch.Count 0) { break; } foreach (var order in batch) { var (_, tableName) ShardRouter.GetRoute(order.Id); // 这里按分片路由逐条写入目标表 // 生产环境建议使用批量写入 幂等控制 var insertSql $ INSERT INTO {tableName} (Id, UserId, ProductName, Amount, Status, CreateTime) VALUES (Id, UserId, ProductName, Amount, Status, CreateTime); ; await using var targetConnection _connectionFactory.CreateConnection(GetDbName(order.Id)); await targetConnection.ExecuteAsync(insertSql, order); total; } lastId batch[^1].Id; Console.WriteLine($迁移进度已完成 {total} 条); } return total; } private string GetDbName(long orderId) { var (dbName, _) ShardRouter.GetRoute(orderId); return dbName; } }这段代码需要注意两个工程问题幂等性迁移过程中如果程序中途崩溃重新启动后可能重复插入数据。生产环境建议在目标表上建立唯一键或用INSERT IGNORE/ON DUPLICATE KEY UPDATE。校验对账迁移完成后不能只看日志说成功就认为成功。要分别统计源表数量和目标分片总数并抽样比对关键字段。数据合并则是另一个方向。比如分片之后报表统计需要汇总所有分片的数据。最简单可靠的方案是定时任务在低峰期把各分片数据汇总到一张统计宽表查询时直接查汇总表。这种方式虽然有一定延迟但实现简单报表压力也不会打到核心分片表上。6. AI 辅助落地的具体用法现在回到标题里的另一个关键词AI。分库分表落地过程中AI 编码工具确实能帮上忙但它的定位是“辅助”不是“全自动”。下面分享几个我能落地使用的场景。6.1 用 AI 生成分片路由代码当你明确了分片策略后可以让 AI 帮你生成基础代码框架。示例提示词请帮我生成一段 C# 方法根据订单号 long 类型将订单均匀路由到 2 个数据库、每个库 4 张表。要求 1. 使用 MD5 哈希后取模避免连续订单 ID 导致热点 2. 返回库名和表名 3. 表名使用 t_order_{index} 格式 4. 给出完整的静态类代码。AI 会给出类似本文ShardRouter的代码。这时候不要直接复制重点检查以下几点取模基数是否等于库数量乘以每库表数量。哈希算法在容器多实例下是否稳定。是否有表名白名单校验。单元测试是否覆盖边界情况。6.2 用 AI 优化慢 SQL当你发现某个分片查询很慢时可以把真实执行计划交给 AI 分析。示例提示词下面是一条 MySQL 慢 SQL请帮我分析可能的原因并给出优化建议 SELECT * FROM t_order_3 WHERE UserId 12345 ORDER BY CreateTime DESC LIMIT 10; 执行计划显示 typeALL扫描行数 200 万。AI 通常能快速指出缺少索引的问题并给出联合索引建议。但注意AI 的建议只是参考最终必须自己在数据库上执行EXPLAIN验证。6.3 用 AI 生成接口压测脚本分库分表方案上线前压测是必须的一步。AI 可以帮你快速生成一个简单的并发压测脚本。示例提示词请帮我生成一个 C# 控制台压测脚本使用 HttpClient 并发调用 POST http://localhost:5000/api/order 1000 次最后统计成功率和平均耗时。AI 生成的脚本可以直接用于本地快速验证。但在生产压测时还是建议使用 k6、JMeter 等专业工具并压测足够多的真实数据量。6.4 AI 辅助的风险提醒AI 生成代码有两个明显风险幻觉 APIAI 可能生成实际不存在的类名或方法编译不过。忽略边界条件分片算法中的溢出、负数、空值问题容易被忽略。所以使用 AI 的底线是生成的代码必须经过 review、编译、单测、压测尤其是涉及分片路由、数据迁移、删除更新这类高风险逻辑时不能过度信任 AI。7. 常见问题排查与最佳实践7.1 分库分表高频问题排查表问题现象常见原因解决思路分片后数据分布不均匀分片键选择不当或取模算法没有散列改用哈希取模检查分片键的取值分布按用户分页查询很慢广播查询到全部分片内存合并开销大引入用户索引表或改用游标分页订单详情查不到写入和查询使用了不同的分片键或路由算法确保写入、查询、迁移共用同一套ShardRouter跨库 join 报错或超时分片键不一致导致跨库关联使用同分片键设计或冗余字段、应用层拼接迁移数据重复迁移脚本没有幂等控制目标表加唯一键使用INSERT IGNORE深度分页慢OFFSET 过大每个分片都要扫描大量数据改用游标分页或限制最大页码分片扩容后历史数据无法查询扩容后取模结果变化使用一致性哈希或先双写再平滑切换7.2 分片键一旦确定尽量不要改这是最重要的工程经验。分片键和路由算法的变更意味着存量数据要全量迁移线上系统要经历一次高风险切换。所以设计阶段一定要先梳理清楚核心查询链路而不是拍脑袋决定按哪列分片。7.3 上线前必须做好的几件事备份分库分表改造前对存量库做全量备份并验证备份可恢复。灰度先接一小部分流量观察路由正确性、延迟和错误率。对账迁移后对比源表和目标表的总数、抽样数据确保一致。回滚预案明确回滚触发条件和操作步骤回滚演练要真正跑一遍。监控对慢 SQL、分片延迟、连接池使用率、路由命中情况建立监控面板。7.4 保持代码层面的可维护性分片逻辑非常核心建议所有数据库访问都收口到仓储层不要在 Controller 或 Service 里散落各种 SQL 拼接。业务层只依赖仓储接口后续即使把自研路由替换成 ShardingCore改动面也限制在仓储实现内部。另外分片路由类建议写成纯静态无状态类方便单元测试。测试用例要覆盖分片键边界值。路由结果是否都在合法分片范围内。同一订单 ID 写入和查询的路由结果是否一致。全部分片是否都能被路由到不存在黑洞表。最后想说的话分库分表是一个“大手术”它解决的是单表大数据量和单库连接瓶颈问题但也会带来分页、联表、分布式事务、数据迁移等新的复杂度。如果你的系统数据量还没到那个量级优先做索引、缓存、读写分离和冷热分离如果数据量确实到了瓶颈也不要紧张从订单、流水这类冷热分明、路由规则明确的数据开始先单库多表再逐步过渡到多库多表。AI 工具在整个过程中可以把编码效率提升很多但它只是辅助。真正保证系统稳定的依然是清晰的分片设计、严格的代码审查、充分的测试和谨慎的上线流程。希望这篇基于 .NET WebAPI 的分库分表实战内容对你有帮助。如果你正在着手改造自己的项目可以从一个简单的订单表尝试入手跑通插入、查询、分页、迁移全流程后再逐步扩展到更复杂的业务。
返回列表