分库分表在很多 .NET 团队里一直是个“听过但不敢动”的话题:单表数据量一上去,接口变慢,分页卡顿,联表查询更是直接超时。但真要动手拆库拆表,又怕路由写错、分页数据重复、跨库 Join 直接翻车。这篇文章就把 .NET WebAPI 高并发场景下的分库分表方案拆开讲清楚,重点解决三个痛点:大数据量分页、联表查询、数据拆分合并,同时给出 AI 辅助落地的具体用法。
先给结论:分库分表不是银弹,但如果你已经遇到“单表千万级数据、单库写入触及瓶颈、分页从几十毫秒变成几秒”这类问题,它就是一个必须掌握的手段。本文会从分片策略设计、路由规则实现、全局分页、跨分片联表、扩容迁移、批量任务、性能调优到 AI 辅助开发,给出一套可以照着落地的思路和代码示例。
这篇文章适合以下读者:已经在用 .NET 6+ 做 WebAPI、业务数据量开始变大、想提前了解分库分表设计方案;或者项目已经被单表大数据量卡住,需要一套可落地的优化路径。内容偏实战,代码部分都能直接复制到项目里改着用。
1. 分库分表方案核心能力速览
| 能力项 | 说明 |
|---|---|
| 目标场景 | 单表数据量过大、写入并发高、分页查询慢、单库存储瓶颈 |
| 分片策略 | 哈希取模、范围分片、日期分片,按业务查询维度选择 |
| 核心技术 | 分片路由规则、多分片合并分页、应用层联表查询、批量迁移工具 |
| 常见落地方式 | ShardingCore、EF Core + 自研路由、Dapper + 路由中间件 |
| 关键难点 | 全局分页结果准确性、跨分片数据合并、扩容时数据再平衡 |
| 支持的大数据量场景 | 订单数据、用户数据、日志数据、流水数据、指标数据 |
| AI 辅助点 | 分片键选型评估、分页 SQL 改写、迁移脚本生成、慢查询日志分析 |
| 适合阶段 | 单表数据量达到千万级、写入 TPS 上涨明显、团队有 DBA 或具备数据库运维能力 |
这套方案的核心思路是:把一个大表拆成多个物理分片,用一套统一的访问入口(路由层)把查询请求分发到正确的分片,再把跨分片的结果合并后返回。
2. 高并发场景下什么时候该上分库分表
分库分表的代价不低,先判断清楚需求再动手。
2.1 该上的信号
- 单表行数已经到千万级,即使加了索引,分页深翻页延迟依然很高。
- 单库写入 TPS 接近上限,特别是在秒杀、集中抢购、批量导入等写入密集型场景。
- 日志、流水类数据增长速度很快,单库存储空间和备份恢复时间已经不可接受。
- 业务本身有天然的分片维度,比如订单按用户Id查询、流水按时间查询,适合横向拆库拆表。
2.2 不该上的情况
- 慢查询只是索引缺失或 SQL 写得不好,先优化索引和语句,没必要拆表。
- 读多写少,优先上读写分离和缓存,性价比远高于分库分表。
- 业务主查询维度不固定,今天按用户查、明天按店铺查、后天按商品查,分片键很难选,拆完反而更慢。
2.3 使用边界和风险
分库分表之后,跨分片事务变成分布式事务,代价明显变高,强一致场景要慎用。跨分片 Join 不再是一个 SQL 能解决的事,需要在应用层做数据合并。扩容时旧分片数据需要重新分布,整个过程必须有演练和回滚方案。涉及资金流水、用户隐私、企业敏感信息的数据,拆分和迁移前必须过合规评审,确认访问权限、审计日志、加密存储都符合要求。
3. 架构设计与分片策略
分库分表方案里,分片键和分片策略是地基,这一步错了后面全部跟着错。
3.1 总体分层
请求链路通常是这样的:
- 接入层:WebAPI 接收请求,做参数校验、身份认证、限流。
- 路由层:根据分片键计算请求应该访问哪个库、哪张表。
- 数据访问层:执行具体查询,多个分片需要并行访问时统一控制并发。
- 存储层:多个数据库实例和表分组,物理上彼此独立。
路由层是最重要的一层,既要负责读,也要负责写,还要为分页、批量任务提供能力。建议把这层独立出来,不要在 Controller 里到处写分片逻辑。
3.2 分片键怎么选
分片键的选择要符合绝大多数业务查询的主维度。比如订单系统,用户下单后最常见的查询是“我的订单”,那 user_id 就是合适的分片键。金融流水系统,最常见的查询是“某天某账户流水”,那日期或账户号就是分片键。如果某个查询没有携带分片键,就必须走广播到所有分片再合并的逻辑,这种查询要尽量控制频率,或通过辅助索引表、ES 等方式承担。
不建议把主键 ID 直接做强hash分片,除非业务只按 ID 查询。很多项目喜欢用雪花ID 做分片,但如果业务查询都是按业务编号来,反而每次都广播。
3.3 分片策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 哈希取模 | 数据分布均匀,实现简单 | 扩容时数据迁移成本高 | 写入并发高,数据量增长平稳 |
| 一致性哈希 | 扩容时迁移数据量小 | 实现复杂度高,部分数据分布可能不均匀 | 数据规模持续增长,扩容频繁 |
| 范围分片 | 范围查询友好,便于按段归档 | 热点问题明显,一个范围库压力大 | 按时间或序号访问明显的业务 |
| 日期分片 | 冷热数据天然分离,历史表可归档 | 单日数据量如果很大会成为热点 | 日志、流水类数据 |
从实际项目看,订单、用户这类业务多数优先使用哈希取模;时序数据、日志数据优先使用日期分片。混合策略也可以,比如先按区域范围分库,再按 ID 取模分表。
3.4 路由表设计
如果分片键需要平滑扩容,可以考虑维护一张路由元数据表,记录每条数据或每个业务实体落在哪个分片。路由表本身量级不大,也可以加缓存。路由元数据表的设计至少包含:
CREATE TABLE shard_route ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_key VARCHAR(64) NOT NULL, shard_db INT NOT NULL, shard_table INT NOT NULL, create_time DATETIME NOT NULL, INDEX idx_biz_key (biz_key) );不过路由表只适合键少、范围小的场景。如果每个用户一条路由,量也不小,还是建议直接通过算法计算分片位置,不落路由表。最常见的做法是:分片键 + 分片总数用哈希取模直接计算。
4. 环境准备与项目初始化
4.1 技术选型
- 操作系统:Windows / Linux 均可。
- 开发框架:.NET 8 或更高版本,WebAPI 项目。
- 数据库:MySQL / PostgreSQL / SQL Server,分库分表逻辑与数据库类型部分解耦。
- ORM:EF Core、Dapper、SqlSugar 都可以作为基础数据访问组件。
- 分库分表组件:可以使用开源 ShardingCore,也可以自己实现轻量路由。这里侧重讲通用实现思路,不依赖某个具体组件的最新 API。
4.2 初始化项目
先用命令创建一个 WebAPI 项目:
dotnet new webapi -n OrderService.Api cd OrderService.Api dotnet add package Dapper dotnet add package MySqlConnector如果使用 EF Core,可以替换为对应的 EF Core 包。下面代码统一使用 Dapper 做示例,因为它的 SQL 控制更直观,适合理解分片路由过程。
4.3 准备分库分表环境
模拟两个库、两个表的环境:
order_db_0 - t_order_0 - t_order_1 order_db_1 - t_order_0 - t_order_1所有分片表的建表语句一致,只是物理位置不同。
CREATE TABLE `t_order_0` ( `id` BIGINT NOT NULL, `order_no` VARCHAR(64) NOT NULL, `user_id` BIGINT NOT NULL, `amount` DECIMAL(12, 2) NOT NULL, `status` INT NOT NULL, `create_time` DATETIME NOT NULL, PRIMARY KEY (`id`), INDEX idx_user_id (`user_id`) );分片规则:以 user_id 作为分片键,分库总数 dbCount=2,每库分表数 tableCount=2,总共 4 个分片。分片位置计算公式为:
shardId = user_id % (dbCount * tableCount) dbIndex = shardId / tableCount tableIndex = shardId % tableCount这样同一个用户的所有订单都固定落在同一个分片表,后续按用户联表查询和分页都会方便很多。
5. 核心路由规则实现
路由层是分库分表落地最核心的一步。
5.1 分片计算工具
public static class ShardingHelper { public const int DbCount = 2; public const int TableCount = 2; public static (int dbIndex, int tableIndex) GetShard(long userId) { long shardId = userId % (DbCount * TableCount); int dbIndex = (int)(shardId / TableCount); int tableIndex = (int)(shardId % TableCount); return (dbIndex, tableIndex); } public static string GetTableName(long userId) { var (_, tableIndex) = GetShard(userId); return $"t_order_{tableIndex}"; } public static string GetConnectionString(int dbIndex) { // 实际项目从配置文件或配置中心读取 return dbIndex == 0 ? "Server=127.0.0.1;Port=3306;Database=order_db_0;Uid=root;Pwd=123456;" : "Server=127.0.0.1;Port=3306;Database=order_db_1;Uid=root;Pwd=123456;"; } }这个工具类做了三件事:根据 user_id 计算分库序号、计算分表序号、拼接表名和连接串。实际项目中,连接字符串应该从配置中心读取,分片数量要设计成可配置,不要写死在代码里。
5.2 写入数据
public async Task<int> InsertOrderAsync(OrderEntity order) { var (dbIndex, _) = ShardingHelper.GetShard(order.UserId); string tableName = ShardingHelper.GetTableName(order.UserId); string connectionString = ShardingHelper.GetConnectionString(dbIndex); string sql = $@"INSERT INTO {tableName} (id, order_no, user_id, amount, status, create_time) VALUES (@Id, @OrderNo, @UserId, @Amount, @Status, @CreateTime)"; using var connection = new MySqlConnection(connectionString); return await connection.ExecuteAsync(sql, order); }这里需要注意:表名没有通过参数传入,而是拼接进去。由于分片序号来自算法计算,实际只会落到合法的 t_order_0 或 t_order_1,所以 SQL 注入风险有限,但生产项目中最好再对表名做一次白名单校验。
5.3 按分片键查询
public async Task<OrderEntity> GetLatestOrderAsync(long userId) { var (dbIndex, _) = ShardingHelper.GetShard(userId); string tableName = ShardingHelper.GetTableName(userId); string connectionString = ShardingHelper.GetConnectionString(dbIndex); string sql = $@"SELECT * FROM {tableName} WHERE user_id = @UserId ORDER BY create_time DESC LIMIT 1"; using var connection = new MySqlConnection(connectionString); return await connection.QueryFirstOrDefaultAsync<OrderEntity>(sql, new { UserId = userId }); }这套逻辑的关键是:只要查询条件带上 user_id,就能在毫秒级定位到唯一分片,不需要扫描所有库表。
6. 全局分页与联表查询实战
分库分表之后,原来的LIMIT @offset, @size会直接失效。原因很简单:每张表只保存全量数据的一部分,按每张表各自的偏移量取数据,合并后不是真实的全局页码。
6.1 分页的几种落地方式
| 方式 | 思路 | 优点 | 缺点 |
|---|---|---|---|
| 应用层全量合并 | 所有分片查询后内存排序再取页 | 实现简单,功能完整 | 深分页时内存和耗时不可控 |
| 并行 Top N 合并 | 每个分片取 Top N,再合并排序 | 性能更好,结果准确 | 逻辑稍复杂 |
| 游标分页 | 用 create_time 和 id 做游标,不依赖页码 | 大数据量下性能最好 | 不适合跳页场景 |
| 走 ES 等搜索引擎 | 分片索引同步到 ES,分页由 ES 承担 | 解决深分页体验 | 引入额外组件,有同步延迟 |
推荐顺序:优先游标分页,其次并行 Top N 合并,最后才是应用层全量合并。用户端最常见的“我的订单列表”场景,下拉加载更适合游标分页。
6.2 并行 Top N 分页示例
public async Task<PagedResult<OrderEntity>> PageOrdersAsync(long userId, int page, int size) { var tasks = new List<Task<IEnumerable<OrderEntity>>>(); for (int dbIndex = 0; dbIndex < ShardingHelper.DbCount; dbIndex++) { string connectionString = ShardingHelper.GetConnectionString(dbIndex); string tableName = ShardingHelper.GetTableName(userId); string sql = $@"SELECT * FROM {tableName} WHERE user_id = @UserId ORDER BY create_time DESC LIMIT @Take"; tasks.Add(QueryShardAsync(connectionString, sql, new { UserId = userId, Take = page * size })); } var results = await Task.WhenAll(tasks); var merged = results .SelectMany(x => x) .OrderByDescending(x => x.CreateTime) .ThenByDescending(x => x.Id); int total = merged.Count(); var items = merged.Skip((page - 1) * size).Take(size).ToList(); return new PagedResult<OrderEntity> { Items = items, Total = total, Page = page, Size = size }; } private static async Task<IEnumerable<OrderEntity>> QueryShardAsync( string connectionString, string sql, object parameters) { using var connection = new MySqlConnection(connectionString); return await connection.QueryAsync<OrderEntity>(sql, parameters); }这个方案的关键思路:每个分片最多取page * size条,应用层合并后再取当前页数据。page 不能太大,否则每个分片拿到的数据量会线性增长。一般建议 page * size 控制在 2000 以内。
6.3 联表查询的改造思路
分库分表之后,跨分片 Join 在数据库层基本行不通,除非两张表的数据恰好通过同一个分片键落到同一物理分片。改造方向有三个:
第一,同分片冗余字段。比如查询订单详情时要展示商品名称,可以在订单表冗余一个商品名称。更新商品名称时,通过异步任务同步冗余字段。
第二,应用层二次查询。先查主表数据,拿到关联 ID 列表后,再批量查关联表,最后在内存中做映射。
var orders = await GetOrdersByUserIdAsync(userId, page, size); var orderIds = orders.Select(x => x.Id).ToList(); var details = await GetOrderDetailsByOrderIdsAsync(orderIds); var detailDict = details.ToLookup(x => x.OrderId); foreach (var order in orders) { order.Details = detailDict[order.Id].ToList(); }第三,构建宽表或搜索引擎。如果业务查询特别复杂,比如按商品查订单、按店铺查流水、多维筛选,那直接在数据库层面做关联已经没有意义,更合理的方案是把查询数据同步到 ES 或ClickHouse,由专门的查询引擎承担。
6.4 分页结果稳定的注意事项
并行分片查询时,每个分片的排序字段必须完全一致,否则合并结果不稳定。如果按时间排序,建议排序字段要带上唯一 ID 作为第二排序键,防止同一时间戳的数据在不同分片返回顺序不同。全局总条数如果每页都统计,代价会很高。常见做法是缓存总条数,或者只在第一页时统计一次。
7. 数据拆分合并与扩容迁移
分库分表方案绕不开扩容和数据迁移。从 4 个分片扩到 8 个分片,如果直接改取模规则,老数据位置全部变了,线上直接雪崩。
7.1 提前确定路由规则
扩容时建议使用一致性哈希,或采用“固定时分片加映射表”的方式。更轻量的方案是分两步走:
- 第一步,新增空分片。
- 第二步,按分片键把老分片数据迁移到新分片,每个分片只动一部分数据。
如果业务允许,在数据模型设计时就预留足够多的分片数,比如直接拆 128 个逻辑分片,先把物理分片少做一些,后续通过调整“虚拟分片到物理分片的映射”来扩容。这样可以减少二次迁移。
7.2 批量数据迁移示例
迁移逻辑一般为:按源分片分批读取,写入目标分片,记录迁移进度,最后做数据校验。
public async Task MigrateShardDataAsync(long userId, int sourceDb, int targetDb) { string sourceConn = ShardingHelper.GetConnectionString(sourceDb); string targetConn = ShardingHelper.GetConnectionString(targetDb); string sourceTable = $"t_order_{ShardingHelper.GetTableName(userId).Split('_')[2]}"; string targetTable = sourceTable; int batchSize = 5000; while (true) { string selectSql = $@"SELECT * FROM {sourceTable} WHERE id > @LastId ORDER BY id LIMIT @BatchSize"; using var sourceConnection = new MySqlConnection(sourceConn); var batch = await sourceConnection.QueryAsync<OrderEntity>( selectSql, new { LastId = lastId, BatchSize = batchSize }); if (batch == null || !batch.Any()) { break; } await BulkInsertAsync(targetConn, targetTable, batch); lastId = batch.Last().Id; // 防止源库压力过大,每批之间稍作停顿 await Task.Delay(50); } }这个示例是通用模板。生产环境必须加断点续传、失败重试、数据比对、迁移日志和回滚机制。
7.3 拆分与合并的常见场景
数据拆分不只是扩容。冷热数据也非常适合拆:把半年前的历史订单拆到历史库,主库只保留热数据。这样查询性能提升明显,备份恢复时间也能缩短。合并则适用于数据量减少、分片浪费严重的场景,比如大量子表数据被清理后,把两个分片合并到一个物理分片。
无论拆分还是合并,都要遵守一个原则:不能直接在线上对同一个热点表做长时间锁表迁移。必须低峰期执行,而且迁移期间写入链路要有写入暂停或双写机制。
8. 高并发 API 设计与批量任务
分库分表解决了存储和查询水平扩展问题,但高并发下 API 还有几个地方要处理。
8.1 幂等设计
批量任务或客户端重试会导致重复写入。订单表建议增加唯一业务索引,比如 order_no 全局唯一。
string sql = $@"INSERT INTO {tableName} (id, order_no, user_id, amount, status, create_time) VALUES (@Id, @OrderNo, @UserId, @Amount, @Status, @CreateTime) ON DUPLICATE KEY UPDATE order_no = VALUES(order_no)";8.2 批量任务队列
数据拆分合并、归档、报表生成这类任务,不能直接塞进普通接口同步执行。建议拆成后台任务队列,分片并行执行,失败自动重试。
public class BatchArchiveJob { private readonly ILogger<BatchArchiveJob> _logger; public async Task RunAsync(CancellationToken ct) { for (int dbIndex = 0; dbIndex < ShardingHelper.DbCount; dbIndex++) { await ArchiveShardAsync(dbIndex, ct); } } private async Task ArchiveShardAsync(int dbIndex, CancellationToken ct) { string connectionString = ShardingHelper.GetConnectionString(dbIndex); string sql = @"UPDATE t_order_0 SET status = @ArchiveStatus WHERE create_time < @CutoffTime LIMIT 2000"; using var connection = new MySqlConnection(connectionString); int rows = -1; while (rows != 0) { rows = await connection.ExecuteAsync(sql, new { ArchiveStatus = 2, CutoffTime = DateTime.Now.AddMonths(-12) }); await Task.Delay(100); } } }批量任务的通用要求是:单批处理量要可控、任务要记录进度、失败要能续跑、每个分片之间不要互相影响。如果任务处理的数据量很大,建议用真正的消息队列或Job系统管理,而不是放在WebAPI进程里跑。
8.3 接口超时和降级
跨分片合并查询在极端情况下会比单库查询更慢。API 设计上要预留超时和降级策略。比如查询全部订单接口,如果分片数量很多,要给整个合并操作设置一个总的超时时间。某个分片超时时,是返回部分结果还是返回失败,要在接口文档里提前约定清楚。
9. 性能观察与调优方向
分库分表之后,性能问题从“单表慢查询”变成了“路由是否准确、合并是否高效、连接池是否够用”。
9.1 核心观察指标
- P95/P99 延迟:重点是分页接口和跨分片合并接口。
- 每个分片的 QPS 和 TPS:如果少数分片负载明显高于其他分片,说明分片键或分片策略有问题。
- 连接池使用率:并行查询分片时,连接数会成倍增加。
- 慢日志数量:分库后依然会有慢 SQL,要按分片分别排查。
- 索引命中率:分片表和原表一样需要索引。
9.2 调优建议
并行查分片时,要控制并发度。假设有 8 个分片,不能一上来就 8 个 Task 全部并发,连接池很容易被打满。可以考虑用信号量控制同时查询的分片数量。
var semaphore = new SemaphoreSlim(4); var tasks = new List<Task<IEnumerable<OrderEntity>>>(); for (int i = 0; i < ShardingHelper.DbCount; i++) { int dbIndex = i; tasks.Add(Task.Run(async () => { await semaphore.WaitAsync(); try { return await QueryShardAsync(dbIndex, sql, parameters); } finally { semaphore.Release(); } })); }深分页场景不用LIMIT 100000, 20,改游标分页,用 create_time 和 id 组合游标。索引方面,分片表并不需要因为分片就减少索引,但也不要无脑加复合索引,要按分片内的实际查询语句设计。缓存策略上,分片键维度相同的热点数据可以放一级缓存,比如用户订单列表头几页;但注意分库分表后缓存更新要考虑多个分片同时失效的情况。
10. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 分页结果缺失或重复 | 各分片排序规则不一致,或并行查询返回顺序不稳定 | 检查每个分片 SQL 的 ORDER BY 字段,要求排序字段完全相同 | 增加唯一 ID 作为第二排序键 |
| 分页越来越慢 | 使用的是深分页,OFFSET 过大 | 查看执行计划,统计 OFFSET 值 | 改游标分页,限制最大页码 |
| 联表查询超时 | 跨分片数据库 Join 无法命中 | 查看数据库慢日志,确认 SQL 是否带分片键 | 拆成应用层多次查询,或在同分片冗余数据 |
| 写入后查不到数据 | 写入路由和查询路由不一致 | 对比两次计算的 dbIndex 和 tableIndex | 统一使用同一个分片计算工具类 |
| 某张分片表数据明显偏多 | 分片键选择不当或数据本身分布不均 | 统计每个分片行数 | 换分片键或调整哈希取模逻辑 |
| 扩容后旧数据查不到 | 取模基数变了,路由算法产生不同结果 | 对比新旧算法计算出的分片位置 | 使用一致性哈希,或先迁移数据再切换算法 |
| 并行查询时数据库连接耗尽 | 分片并发数量超过连接池上限 | 查看连接池监控 | 用信号量限制并发查询数,调大连接池 |
| 跨分片更新事务失败 | 强一致分布式事务不稳定 | 查看分布式事务日志 | 避免跨分片事务,换成最终一致性方案 |
| 批量迁移中途失败 | 任务没有记录断点,重启后重复或遗漏 | 检查迁移日志和 lastId | 增加迁移进度表,支持断点续传 |
| 接口整体响应变慢 | 广播查询量太大,每次查询都扫所有分片 | 统计接口调用中不带分片键的比例 | 限制无分片键查询,增加查询维度表或 ES |
11. AI 辅助落地的具体做法
标题里提到了 AI 辅助落地,这一点实际使用价值很高。分库分表方案里的很多琐碎工作,用 AI 可以减少重复劳动,但必须做好人工审查。
11.1 用 AI 做分片键选型评估
把业务表结构、查询频率、主要 SQL 语句整理成提示词,让 AI 输出一份分片键对比评估。例如:
我有一张订单表 order,主查询按 user_id 查询订单列表, 后台按 order_no 查询订单详情,日增数据量约 50 万行, 请帮我对比按 user_id、按 create_time、按 order_no 三种 分片键的优缺点,并结合查询频率给出推荐。AI 给出的结论不一定全对,但可以帮你快速梳理思路,尤其是分片键对分页、联表查询、数据倾斜的影响。
11.2 用 AI 改写分页和联表查询 SQL
把原始的深分页 SQL 贴给 AI,要求转换为游标分页写法:
这条 SQL 在单表 2000 万数据下很慢: SELECT * FROM order WHERE user_id = @userId ORDER BY create_time DESC LIMIT 100000, 20; 帮我改写成基于 create_time 和 id 的游标分页写法, 保留 MySQL 兼容性。AI 生成的 SQL 需要在本地真实数据量环境下做执行计划验证,不能直接上线。重点验证索引是否命中、游标条件是否比 OFFSET 更快。
11.3 用 AI 生成数据迁移脚本
给 AI 描述迁移场景:源分片、目标分片、批次大小、需要做断点续传。AI 可以生成一版模板代码,再人工修改成符合项目连接串管理和日志规范的版本。
写一个 .NET 控制台程序,把 order_db_0.t_order_0 中的数据 按每批 5000 条迁移到 order_db_1.t_order_0,需要支持 断点续传和失败重试,使用 Dapper + MySqlConnector。然后结合项目实际的连接串管理方式改造。迁移脚本必须先在测试环境完整跑一遍,确认数据行数一致、无丢失、无重复再上生产。
11.4 用 AI 分析慢查询日志
分库分表之后,慢日志会来自不同分片。把 MySQL 慢日志文件里同一类 SQL 提取出来,让 AI 归纳共性,分析是路由问题、索引问题还是分片键未携带导致广播查询。这样可以更快定位到底是哪类查询在拖垮整体性能。
11.5 AI 辅助的安全边界
使用外部 AI 工具时,严禁把生产环境真实数据、用户手机号、身份证、资金流水等信息直接放入提示词。可以先做脱敏处理,或者使用企业内部私有化部署的模型。AI 生成的所有代码在合入前必须走人工 Code Review,重点检查表名拼接、SQL 注入、事务边界和回滚逻辑。
12. 最佳实践与总结
分库分表方案能落地的前提是路由规则足够简单、分片键足够稳定、合并逻辑足够克制。
- 先优化单表、加索引、上缓存、做读写分离,这些手段不够了再考虑分库分表。
- 分片键确定后尽量不要改,一期设计就要把未来两三年的数据量和查询模式考虑进去。
- 分页接口优先游标分页,不要迷信
LIMIT OFFSET。 - 联表查询优先通过数据冗余和同分片设计解决,应用层 Merge 是兜底方案,不能所有接口都靠它。
- 数据迁移和扩容必须有演练、有回滚、有断点续传,不能在线上裸奔执行。
- 批量任务不要放在请求线程里执行,拆分到 Job 系统,按分片并行处理,控制批次大小。
- 从第一行分片代码开始,日志里就要带 dbIndex 和 tableIndex,否则问题出现后很难定位。
- AI 可以用来做分片键评估、SQL 改写、迁移脚本生成和慢日志分析,但任何 AI 产物都要人工验证后上线。
分库分表最值得先验证的功能是:带分片键的查询能不能稳定定位到唯一分片;不带分片键的兜底查询到底有多慢;分页结果在数据真实分布下是否准确。最容易踩的坑是路由算法写了两套,读写不一致;其次是深分页没改游标,拆完库反而更慢。
先把最小可行环境搭起来,按订单表做一个带分片路由的用户订单查询接口,压测看单分片和多分片合并的性能差异。确认这条链路稳定后,再逐步加订单明细、支付流水、历史归档和批量任务。这套方案在数据量和并发上来之后,能帮你把 WebAPI 的扩展路径留出来,不至于等线上报警才临时拆库。