分库分表原理

单表数据量涨到千万级以后,查询变慢、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 章)
运维排查一条链路跨多个库统一日志链路追踪

什么时候不该分

在下列情况下,先做这些优化往往能省掉整套分库分表:

  1. SQL 与索引问题:加对索引、避免 SELECT *、消除隐式类型转换;
  2. 历史数据归档:把一年前的数据迁到冷库,在线表直接瘦身一半以上;
  3. 读写分离:读多写少的场景,加从库是最省事的扩容方式;
  4. 缓存:热点数据放缓存,数据库压力可能下降一个数量级;
  5. 搜索引擎:复杂查询与大分页交给搜索引擎承担;
  6. 时序/日志数据:改用按时间分区的表或专门的时序存储。

经验判断:如果能靠加索引、加缓存、加从库把问题解决,就不要分库分表。

小结:分库分表用于突破单库单表的容量与写入上限,先按瓶颈判断该做垂直拆分还是水平拆分,再选中间件或应用层分片路线。拆分会同时带来跨库 join、分布式事务、跨库分页、全局 ID、扩容迁移等代价,因此务必先把索引优化、归档、读写分离和缓存做在前面。

笔记加载中…