1. 为什么要预研分库分表
在业务早期,单库单表通常是最简单、最稳定、成本最低的方案。
但当系统中出现一些高频写入的大表时,单库单表会逐渐暴露问题。例如:
- 查询越来越慢。
- 索引越来越大。
- 写入和查询互相影响。
- 大表 DDL 风险变高。
- 历史数据归档困难。
- 慢 SQL 优化空间越来越小。
- 单库资源扩容成本越来越高。
典型的大表包括:
- 消息流水表。
- 设备事件表。
- 操作日志表。
- 交易明细表。
- 监控采集表。
- 时序统计表。
- 审计记录表。
这类表的共同特点是:
- 数据持续增长。
- 写入频率高。
- 查询经常带时间范围。
- 数据天然有业务维度,例如设备、用户、租户、组织。
- 最近数据访问频繁,历史数据访问较少。
所以,分库分表预研的核心问题不是"怎么拆表",而是:
text
系统的核心查询和写入模型,是否适合被稳定地路由到少量分片。
如果不能精准路由,分库分表很可能只是把一个单表慢查询,变成多个分片上的广播查询。
2. 什么时候需要考虑分库分表
分库分表不是越早越好。过早引入会增加开发、测试和运维复杂度。
通常可以从以下维度判断是否需要进入预研阶段。
2.1 数据量维度
可以重点观察:
- 单表行数是否已经达到千万级甚至更高。
- 表数据文件和索引文件是否持续膨胀。
- 历史数据是否明显多于热数据。
- 大表备份、恢复、DDL 是否已经变慢。
数据量只是一个信号,不是唯一标准。
有的表几千万行但查询简单、索引合理,仍然可以稳定运行;有的表几百万行,但查询复杂、索引设计差,也可能很慢。
2.2 查询维度
重点关注:
- 是否出现大量慢 SQL。
- 是否有大 offset 分页。
- 是否频繁按时间范围查询。
- 是否经常只查最近一段时间数据。
- 是否存在不带高选择性条件的模糊查询。
- 是否存在多字段动态组合查询。
如果慢查询主要来自不合理 SQL、缺索引、隐式类型转换、函数导致索引失效,那么应该先做 SQL 和索引优化,不要直接上分库分表。
2.3 写入维度
重点关注:
- 写入吞吐是否接近单库瓶颈。
- 高频写入是否导致索引维护成本过高。
- 写入是否影响查询。
- 是否有批量写入、异步写入、削峰空间。
如果只是瞬时峰值,可以先考虑:
- 批量写入。
- 队列削峰。
- 异步落库。
- 读写分离。
- 表分区或归档。
2.4 运维维度
分库分表会引入新的运维问题:
- 分片元数据管理。
- 分片扩容。
- 数据迁移。
- Proxy 高可用。
- 分布式事务。
- 跨分片聚合。
- 监控和告警。
如果团队没有足够的运维和排障能力,建议先通过更保守的方式优化。
3. 分库分表前可以先做哪些优化
在正式拆表前,可以先做一轮低成本优化:
- 补齐必要索引。
- 清理无效索引。
- 消除隐式类型转换。
- 避免函数作用在索引列上。
- 大分页改成游标分页。
- 冷热数据归档。
- 读写分离。
- 报表查询走汇总表。
- 搜索类查询走搜索引擎。
- 高频统计走预聚合表。
如果这些手段已经不能满足需求,再考虑分库分表。
4. 目标表如何选择
不是所有表都适合分库分表。
优先处理:
- 写入量最大的流水表。
- 数据增长最快的历史表。
- 查询压力最集中的核心表。
- 历史数据占比高且可按时间归档的表。
不建议优先处理:
- 配置表。
- 字典表。
- 状态变化不频繁的主数据表。
- 数据量不大的关联表。
- 查询不在核心链路上的辅助表。
一个常见策略是:
text
核心大表:分库分表
配置字典表:保持单表
报表统计:汇总表或物化视图
搜索筛选:搜索引擎或索引表
历史数据:归档或冷热分层
5. 分片维度设计
分片维度通常要从查询模型倒推。
5.1 时间维度
高频流水表大多天然适合按时间拆分。
常见粒度:
text
按天:yyyyMMdd
按周:yyyy_WW
按月:yyyyMM
按季度:yyyy_Q
按时间拆分的好处:
- 历史归档方便。
- 查询最近数据时扫描范围小。
- 按时间删除旧数据成本低。
- 单表大小可控。
缺点:
- 时间范围过大时会跨多张表。
- 表数量会随着时间增长。
- 需要处理跨表聚合和分页。
一般建议从按月或按周开始,极高频写入场景再考虑按天。
5.2 业务维度
除了时间,还需要选择一个能稳定路由的业务维度。
常见选择:
text
device_id
user_id
tenant_id
org_id
account_id
order_id
选择分片键时要看:
- 核心查询是否一定带这个字段。
- 字段分布是否均匀。
- 是否容易产生热点。
- 关联写入是否能落到同一分片。
- 后续扩容和迁移成本是否可控。
不适合作为分片键的字段:
- 状态。
- 类型。
- 布尔值。
- 低基数字段。
- 经常为空的字段。
5.3 时间 + 哈希组合
对于高频流水表,一个比较常见的设计是:
text
t_message_yyyyMM_hashN
例如:
text
t_message_202601_00
t_message_202601_01
t_message_202602_00
t_message_202602_01
路由逻辑:
text
先根据时间确定月份表组
再根据业务 ID hash 到具体分片
这样可以同时控制:
- 单表数据量。
- 单分片写入压力。
- 历史归档范围。
6. 分片算法
6.1 哈希取模
最常见的算法:
text
hash(sharding_key) % N
优点:
- 实现简单。
- 数据分布相对均匀。
- 同一个分片键能稳定落到同一分片。
缺点:
- N 变化后,大量数据需要迁移。
- 扩容成本较高。
适合初期分片数量相对稳定的场景。
6.2 一致性哈希
一致性哈希适合需要后续扩容的场景。
优点:
- 扩容时迁移的数据量相对较少。
- 可通过虚拟节点改善分布。
缺点:
- 实现和运维复杂度更高。
- 对中间件支持情况有要求。
6.3 范围分片
范围分片常用于时间字段。
例如:
text
2026-01 -> t_message_202601
2026-02 -> t_message_202602
优点:
- 便于历史数据清理。
- 查询时间范围时容易裁剪分片。
缺点:
- 当前时间分片可能成为写入热点。
- 时间范围过大时会跨很多表。
7. 技术方案选型
7.1 ShardingSphere-JDBC
ShardingSphere-JDBC 是应用内嵌式方案。
优点:
- 功能完整。
- 生态成熟。
- 支持分库分表、读写分离、分布式事务、数据加密等能力。
- 文档和案例较多。
不足:
- 主要面向 JDBC 生态。
- 对纯 R2DBC 项目不如 Proxy 自然。
- 应用侧引入成本更高。
7.2 ShardingSphere-Proxy
对于 WebFlux + R2DBC 场景,更推荐优先评估 ShardingSphere-Proxy。
架构形态:
text
应用服务 -> R2DBC Driver -> ShardingSphere-Proxy -> 后端数据库
应用仍然以数据库方式连接,只是连接目标从真实数据库变成了 Proxy。
优点:
- 对应用侵入较小。
- 对 R2DBC 项目更友好。
- 分片规则集中管理。
- 可以统一处理读写分离和分片路由。
- 后续数据库迁移时,应用侧改动相对可控。
风险:
- Proxy 本身成为关键链路。
- 需要额外部署和监控。
- 跨分片聚合仍然有成本。
- 复杂 SQL 兼容性需要压测验证。
7.3 自研路由
自研路由通常是应用内根据业务参数计算物理库表。
示例:
text
业务参数 -> 路由规则 -> DataSource / ConnectionFactory -> 物理表名
优点:
- 性能可控。
- 逻辑贴合业务。
- 可以针对特殊查询做深度优化。
缺点:
- 研发和维护成本高。
- 跨分片查询要自己处理。
- 扩容和迁移要自己处理。
- 事务边界更复杂。
除非团队有明确的中间件维护能力,否则不建议一开始就完全自研。
8. 推荐架构
更稳妥的落地路线是:
text
ShardingSphere-Proxy + 汇总表 + 索引表 + 冷热归档
职责划分:
- Proxy 负责普通分片路由。
- 汇总表负责报表统计。
- 索引表负责非分片键查询。
- 搜索引擎负责复杂检索。
- 冷热归档负责历史数据生命周期。
这样能避免把所有复杂度都压在分库分表中间件上。
9. 查询改造重点
分库分表后,查询模型需要同步改造。
9.1 核心查询必须带分片键
理想查询:
sql
WHERE device_id = ?
AND create_time >= ?
AND create_time < ?
这类查询可以根据设备和时间精准路由。
不理想查询:
sql
WHERE status = ?
ORDER BY create_time DESC
LIMIT 20
这种查询没有分片键,容易广播到所有分片。
9.2 非分片键查询需要辅助结构
如果业务确实需要按非分片键查,例如状态、手机号、外部编号,可以考虑:
- 索引表。
- 搜索引擎。
- 汇总表。
- 缓存映射。
例如:
text
external_no -> sharding_key
先通过索引表找到分片键,再查询真实分片表。
9.3 跨分片分页要谨慎
传统分页:
sql
LIMIT offset, size
跨分片后可能变成每个分片都取 offset + size 条,再聚合排序。
offset 越大,性能越差。
建议改成游标分页:
sql
WHERE create_time < last_create_time
ORDER BY create_time DESC
LIMIT 20
或者:
sql
WHERE id > last_id
ORDER BY id
LIMIT 20
9.4 排序字段要稳定
跨分片排序时,单独按时间排序可能不稳定。
建议使用组合排序:
sql
ORDER BY create_time DESC, id DESC
这样可以避免同一时间点多条数据时分页重复或丢失。
10. 写入改造重点
10.1 全局唯一 ID
分库分表后,不建议依赖数据库自增主键。
常见方案:
- UUID。
- 雪花算法。
- 号段模式。
建议优先考虑雪花算法或号段模式。
UUID 虽然简单,但作为主键时通常不够友好:
- 无序。
- 索引膨胀。
- 写入局部性差。
10.2 批量写入
批量写入需要注意:
- 单批大小。
- 参数数量限制。
- 连接池占用。
- 事务范围。
- 失败重试。
如果一批数据会落到多个分片,需要评估是否拆批:
text
按目标分片分组 -> 分批写入 -> 单分片事务
10.3 幂等写入
分库分表后,重复写入和补偿写入更常见。
建议设计:
- 全局唯一业务 ID。
- 唯一索引。
- 幂等插入。
- 可重试的补偿逻辑。
11. 事务问题
分库分表后,单库事务不一定能覆盖完整业务操作。
要优先通过设计规避分布式事务:
- 同一业务聚合内的数据尽量落到同一分片。
- 跨分片操作尽量拆成异步流程。
- 能最终一致的场景,不强求强一致。
- 核心强一致场景再评估分布式事务。
如果必须跨分片事务,可以评估:
- XA。
- BASE。
- Seata。
- 事务消息。
- 本地消息表。
需要注意的是,分布式事务会带来性能和复杂度成本,不应作为默认方案。
12. 数据迁移方案
在线系统改造分库分表时,数据迁移是难点。
常见步骤:
- 建新分片表。
- 增加双写或变更捕获。
- 全量迁移历史数据。
- 增量同步新数据。
- 校验数据一致性。
- 灰度切读。
- 灰度切写。
- 保留回滚窗口。
- 下线旧表或归档。
校验维度包括:
- 行数。
- 主键集合。
- 核心字段 hash。
- 时间范围抽样。
- 业务接口比对。
不要只看迁移任务成功,还要验证业务查询结果是否一致。
13. MySQL 切换 PostgreSQL 的注意点
如果系统后续可能从 MySQL 切换到 PostgreSQL,分库分表设计时要尽量减少数据库方言绑定。
13.1 驱动差异
R2DBC 驱动不同:
text
MySQL: r2dbc-mysql
PostgreSQL: r2dbc-postgresql
如果使用 ShardingSphere-Proxy,应用连接 Proxy,后端数据库类型变化主要体现在 Proxy 配置和 SQL 方言兼容性上。
13.2 自增主键
MySQL:
sql
AUTO_INCREMENT
PostgreSQL:
sql
SERIAL
BIGSERIAL
GENERATED AS IDENTITY
分库分表场景建议使用分布式 ID,避免绑定数据库自增能力。
13.3 时间函数
MySQL:
sql
NOW()
DATE_FORMAT()
UNIX_TIMESTAMP()
PostgreSQL:
sql
now()
to_char()
extract()
date_trunc()
建议将时间计算统一封装,避免业务 SQL 到处散落数据库函数。
13.4 Upsert 语法
MySQL:
sql
INSERT ... ON DUPLICATE KEY UPDATE
PostgreSQL:
sql
INSERT ... ON CONFLICT (key) DO UPDATE
如果业务大量依赖幂等写,建议抽象统一写入接口,再按数据库方言生成 SQL。
13.5 JSON 差异
MySQL 和 PostgreSQL 的 JSON 能力差异较大。
PostgreSQL 的 jsonb、表达式索引、部分索引能力更强,但 SQL 写法不同。
建议:
- 高频查询字段不要长期放在 JSON 中。
- 核心过滤字段拆成普通列。
- JSON 用于保存扩展属性。
13.6 索引能力
PostgreSQL 可以重点评估:
- btree index。
- partial index。
- expression index。
- BRIN index。
对于按时间递增的大表,BRIN 索引在某些范围查询中非常有价值。
13.7 事务隔离级别
MySQL InnoDB 默认通常是:
text
Repeatable Read
PostgreSQL 默认是:
text
Read Committed
如果业务依赖特定一致性语义,迁移前必须验证。
14. 压测和验证指标
分库分表不是配置完成就结束,必须压测。
建议至少验证:
- 单写吞吐。
- 批量写入吞吐。
- 单分片查询耗时。
- 跨分片查询耗时。
- 分页查询耗时。
- Proxy 路由耗时。
- Proxy CPU 和内存。
- 后端连接池使用率。
- 慢 SQL 数量。
- P95 / P99 响应时间。
建议对比三组数据:
text
单库单表基线
分片后单分片查询
分片后跨分片查询
只有这样才能判断分库分表到底有没有实际收益。
15. 监控和运维
上线后需要监控:
- Proxy 存活状态。
- Proxy 路由耗时。
- Proxy 后端连接数。
- 数据库连接池。
- 分片表数据量。
- 分片热点。
- 慢 SQL。
- 跨分片查询数量。
- 迁移任务状态。
- 数据一致性校验结果。
分库分表后的系统,问题往往不再只发生在数据库,也可能发生在 Proxy、连接池、路由规则、应用 SQL 和数据迁移链路上。
16. 落地路线建议
比较稳妥的路线:
- 梳理核心大表和慢查询。
- 先做索引、SQL、归档和读写分离优化。
- 明确核心查询是否能带分片键。
- 设计时间分片和业务哈希分片。
- 搭建 ShardingSphere-Proxy 验证环境。
- 编写核心 SQL 兼容性测试。
- 做压测和容量评估。
- 设计迁移和回滚方案。
- 小流量灰度。
- 逐步切换读写流量。
不要一开始就全量切换。
17. 最终 checklist
分库分表落地前,建议确认:
- 是否已经证明单库单表成为瓶颈。
- 是否已经做过低成本优化。
- 分片键是否稳定。
- 核心查询是否都能带分片键。
- 是否有跨分片查询兜底方案。
- 是否有全局 ID 方案。
- 是否避免了大 offset 分页。
- 是否有数据迁移方案。
- 是否有数据校验方案。
- 是否有回滚方案。
- 是否压测过核心链路。
- 是否监控 Proxy 和后端数据库。
- 是否评估过 MySQL / PostgreSQL 方言差异。
18. 总结
分库分表不是简单地把一张大表拆成多张小表,而是一整套数据架构改造。
它会影响:
- 查询模型。
- 写入模型。
- 主键生成。
- 事务边界。
- 分页排序。
- 数据迁移。
- 运维监控。
- 数据库迁移。
对于 WebFlux + R2DBC 场景,ShardingSphere-Proxy 是一个值得优先评估的方案,因为它能把分片能力下沉到代理层,降低应用侧侵入。
但无论选择 Proxy 还是自研路由,最关键的问题仍然是:
text
能否通过分片键精准路由。
如果核心查询无法携带分片键,分库分表可能不会带来预期收益,反而会引入更多跨分片聚合、分页、事务和运维问题。
一句话总结:
text
分库分表的价值,不在于"拆",而在于让核心读写能够稳定落到可控范围内。