分库分表原理
单表数据量涨到千万级以后,查询变慢、DDL 卡死、备份恢复时间越来越长;单库写入到几千 TPS 后,磁盘 IOPS 与连接数开始成为硬瓶颈。分库分表是把一份数据按规则拆到多个库、多个表的技术。它解决容量问题,但会引入跨库查询、分布式事务、扩容迁移等一连串新问题,因此它应该是最后手段,而不是第一选择。
单表到底卡在哪里
先明确瓶颈来自哪个方向,才能判断该不该拆。
| 瓶颈 | 表现 | 根因 |
|---|---|---|
| 数据量 | 查询 TP99 随行数线性上升 | B+ 树层数增加,随机 IO 变多 |
| 写入 | 写入 TPS 上不去 | 单库磁盘 IOPS、redo log 刷盘、行锁竞争 |
| 索引 | 单索引文件过大,无法全部缓存 | Buffer Pool 装不下,命中率下降 |
| 运维 | 加字段锁表、备份恢复慢 | 单表文件过大,DDL 与备份都是全量操作 |
| 连接 | 连接数打满 | 单实例可承载的连接数有限 |
参考量级(经验值,实际取决于行大小、字段类型与硬件):
单表行数 :千万级开始明显变慢,亿级后问题集中爆发
单库写入 :普通机械盘数千 TPS,SSD 可更高,但受刷盘策略影响
B+ 树高度 :约 2000 万行时通常为 3 层,再翻倍就可能变 4 层
注意:慢查询往往不是数据量造成的,而是缺索引、索引失效或返回大量字段。分表解决不了 SQL 写错的问题,先优化 SQL 再考虑分表。
垂直拆分与水平拆分
两种拆分维度完全不同,解决的问题也不同。
| 对比项 | 垂直拆分 | 水平拆分 |
|---|---|---|
| 拆分依据 | 按业务或字段 | 按数据行 |
| 拆分后 | 每张表字段更少 | 每张表结构相同、数据不同 |
| 解决什么 | 表太宽、冷热字段混在一起 | 单表行数太多 |
| 典型例子 | 订单表拆出订单基础表 + 订单扩展表 | 订单表拆成 t_order_0 ~ t_order_15 |
| 复杂度 | 低,主要是改代码 | 高,涉及路由、扩容、跨片查询 |
垂直拆分还包括垂直分库:按业务域把订单、用户、商品拆到不同数据库实例,这通常是微服务化的自然结果,和性能无关,但能显著降低单库压力。
垂直拆分的示例:
-- 拆分前:一张宽表,热字段与冷字段混在一起
-- orders(id, user_id, amount, status, created_at, remark, ext_json, invoice_info, ...)
-- 拆分后
-- 热表:只放高频查询字段,保证行小、能全部缓存
CREATE TABLE orders_main (
order_no VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status TINYINT NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (order_no),
KEY idx_user (user_id, created_at)
) ENGINE=InnoDB;
-- 扩展表:低频字段,只在详情页查询
CREATE TABLE orders_ext (
order_no VARCHAR(32) NOT NULL,
remark VARCHAR(255) DEFAULT NULL,
ext_json TEXT,
invoice_info TEXT,
PRIMARY KEY (order_no)
) ENGINE=InnoDB;
水平拆分的四种组合
水平拆分按"拆库"和"拆表"两个维度组合,共四种形态:
| 组合 | 形态 | 适用场景 |
|---|---|---|
| 只分表 | 单库内多张同构表 | 单库写入够用,只是单表太大 |
| 只分库 | 每库一张同构表 | 单库连接数/IOPS 不够,数据量不算大 |
| 分库 + 分表 | 多库多表,如 4 库 × 16 表 | 数据量大且写入高,最常见形态 |
| 垂直 + 水平 | 先按业务分库,再各自水平拆 | 大型系统的常规终态 |
分库分表后的数据分布示意:
order_db_0: t_order_0 ~ t_order_15
order_db_1: t_order_0 ~ t_order_15
order_db_2: t_order_0 ~ t_order_15
order_db_3: t_order_0 ~ t_order_15
共 64 张表,按 user_id 的哈希值落到其中一张
分库分表数量的选择原则:单表控制在 500 万 ~ 2000 万行、单库表数量不宜过多(过多会影响文件句柄与元数据管理)。数量一般取 2 的幂,便于后续双倍扩容(见第 09 章)。
中间件方案与应用层分片
谁来解析 SQL、路由到目标库表,有两种路线。
| 对比项 | 中间件(代理/JDBC 层) | 应用层分片 |
|---|---|---|
| 代表 | Apache ShardingSphere(提供 JDBC 与 Proxy 两种接入形态)、MyCat 等 | 自己在 DAO 层算路由 |
| 接入方式 | 对应用基本透明,改配置即可 | 需改造代码与 SQL |
| 功能 | 分片路由、读写分离、分布式主键等由中间件承担 | 全部自己实现 |
| 复杂度 | 运维与排查链条变长,出问题定位更难 | 逻辑全在自己手里,可控性强 |
| 限制 | 对复杂 SQL 支持有限,跨库查询能力受限 | 不做跨库查询,灵活性最高 |
选型建议:
- 团队规模小、希望快速改造,优先考虑中间件,但必须先确认业务 SQL 是否落在中间件支持范围内(尤其是子查询、聚合、多表 join),具体能力以官方文档为准;
- 业务查询模式清晰、追求极致可控,用应用层分片,只允许"带分片键的等值查询"和"按分片键的批量查询";
- 无论哪种路线,都不建议依赖跨库 join,宁可在应用层组装数据。
分库分表的代价清单
拆之前必须把这笔账算清楚,每一条都是真实成本:
| 代价 | 说明 | 常见应对 |
|---|---|---|
| 跨库 join | 订单与用户分属不同库,无法 join | 字段冗余、应用层组装、宽表 |
| 分布式事务 | 跨库写入无法用本地事务 | 最终一致 + 幂等 + 对账 |
| 跨库分页 | LIMIT 100000,20 需各库取再归并 | 禁止深分页、按游标翻页 |
| 全局排序 | 各片结果需在内存归并 | 数据量小才可行,否则改用搜索引擎 |
| 全局唯一 ID | 自增主键失效 | 分布式 ID 方案(见第 10 章) |
| 统计聚合 | COUNT、SUM 需汇总多片 | 异步汇总表或离线计算 |
| DDL 变更 | 需对每张表逐个执行 | 自动化工具 + 灰度执行 |
| 扩容迁移 | 分片数变更需搬迁数据 | 双写 + 校验 + 灰度切换(见第 09 章) |
| 运维排查 | 一条链路跨多个库 | 统一日志链路追踪 |
什么时候不该分
在下列情况下,先做这些优化往往能省掉整套分库分表:
- SQL 与索引问题:加对索引、避免
SELECT *、消除隐式类型转换; - 历史数据归档:把一年前的数据迁到冷库,在线表直接瘦身一半以上;
- 读写分离:读多写少的场景,加从库是最省事的扩容方式;
- 缓存:热点数据放缓存,数据库压力可能下降一个数量级;
- 搜索引擎:复杂查询与大分页交给搜索引擎承担;
- 时序/日志数据:改用按时间分区的表或专门的时序存储。
经验判断:如果能靠加索引、加缓存、加从库把问题解决,就不要分库分表。
小结:分库分表用于突破单库单表的容量与写入上限,先按瓶颈判断该做垂直拆分还是水平拆分,再选中间件或应用层分片路线。拆分会同时带来跨库 join、分布式事务、跨库分页、全局 ID、扩容迁移等代价,因此务必先把索引优化、归档、读写分离和缓存做在前面。