分库分表

分库分表

[!abstract] 一句话理解
分库分表是把原本集中在单机数据库上的数据和访问压力,按规则拆分到多个库、多个表中,以突破容量、性能和扩展性瓶颈。

一、先记住这 5 句话

  1. 分库分表解决的是单机数据库的容量、吞吐和扩展性问题。
  2. 它的本质是用系统复杂度换规模能力。
  3. 不是所有系统都需要分库分表,没有痛点不要硬拆。
  4. 分片键选得是否合理,基本决定方案成败。
  5. 真正麻烦的不是“怎么拆”,而是拆完之后怎么查、怎么排、怎么扩、怎么保证一致性。

二、5W 总览

维度 核心问题 一句话结论
What 什么是分库分表 按规则把数据拆到多个库和表
Why 为什么要做 单库单表扛不住数据量、吞吐和增长
When 什么时候做 常规优化做完后,单机仍是瓶颈再做
Where 按什么维度拆 关键在分片键(Sharding Key)
How 怎么落地 代码路由、中间件、代理层都可以

三、What:什么是分库分表

3.1 四种常见拆分方式

方式 含义 示例 说明
水平分表 按数据行拆到多张表 order -> order_0 ~ order_7 常用于单表过大
水平分库 按数据行拆到多个库 db_order_0 ~ db_order_3 常用于单库容量或吞吐不足
垂直分表 按字段拆到不同表 user_baseuser_profile 让高频字段和低频字段分离
垂直分库 按业务模块拆到不同库 用户库、订单库、支付库 本质上更接近按业务边界拆系统

3.2 一个典型例子

原来只有一张大订单表:

1
orders

拆分后可能变成:

1
2
3
4
db_order_0.orders_0 ~ orders_7
db_order_1.orders_0 ~ orders_7
db_order_2.orders_0 ~ orders_7
db_order_3.orders_0 ~ orders_7

这就是业界最常见的模式之一:先分库,再分表。

3.3 水平拆分和垂直拆分的区别

水平拆分

  • 拆的是“数据行”
  • 目的是分散数据量和访问压力
  • 典型问题是路由、分页、聚合、事务、扩容

垂直拆分

  • 拆的是“字段”或“业务模块”
  • 目的是降低耦合、优化访问模式
  • 典型问题是跨表查询和跨服务协同

示例:

1
user(id, name, age, phone, avatar, intro, address, create_time)

可以拆成:

1
2
user_base(id, name, age, phone, create_time)
user_profile(id, avatar, intro, address)

核心原则:经常一起访问的字段放在一起。

四、Why:为什么要做分库分表

4.1 单库单表的典型瓶颈

问题 常见表现
数据量太大 索引树变高、扫描范围变大、DDL 变慢
写入压力太高 主库 QPS 到顶、IO 打满、锁竞争加重
读取压力太高 热点表读流量过于集中,主从也扛不住
单机容量有限 CPU、内存、磁盘、网络带宽都有上限
运维风险上升 备份恢复慢、扩容难、故障影响范围大

4.2 本质结论

[!important] 本质
分库分表不是“白赚性能”,而是用更高的系统复杂度,换取更高的容量、吞吐和扩展性。

4.3 不要忽略前置优化

在考虑分库分表之前,通常应先尝试:

  • SQL 优化
  • 索引优化
  • 读写分离
  • 缓存
  • 历史数据归档
  • 垂直拆分

如果这些都做了,瓶颈依然明显,再考虑水平拆分。

五、When:什么时候该做

5.1 适合开始评估拆分的信号

  1. 单表数据量持续膨胀到几千万甚至上亿。
  2. 主库写入压力长期接近上限,高峰期明显堆积。
  3. DDL、备份、恢复、扩容越来越难做。
  4. 业务天然具备清晰的拆分维度,如用户、租户、商户、地域。
  5. 业务增长可预期,继续拖延只会让后续迁移更痛苦。

5.2 一个常见误区

[!warning] 不要一开始就拆
很多系统的问题,本质上还停留在 SQL、索引、缓存或冷热分离层面。能在低复杂度阶段解决,就不要过早引入分库分表。

六、Where:按什么维度拆

6.1 什么是分片键

分片键(Sharding Key)是决定一条数据落到哪个库、哪张表的关键字段。

常见分片维度:

  • user_id
  • order_id
  • merchant_id
  • tenant_id
  • region_id
  • 时间维度

6.2 选分片键的 5 个标准

标准 解释
分布均匀 尽量避免数据倾斜和单点热点
查询高频使用 让大部分查询都能精准路由
字段稳定 尽量不要选容易变更的业务字段
不易形成热点 避免超级用户、超级商户打爆单分片
便于扩容 后续增加分片时迁移成本不能过高

6.3 典型分片算法

算法 示例 优点 缺点
取模 user_id % 4 简单、分布通常较均匀 扩容迁移成本高
范围分片 0~1000万 -> 库1 范围查询直观 容易数据倾斜
一致性哈希 哈希环路由 扩容迁移量较小 实现和治理更复杂
时间分片 orders_202601 便于归档和冷热分离 热点集中、跨时间查询麻烦

6.4 选错分片键会怎样

  • 查询经常打到多个分片
  • 广播查询变多
  • 热点集中在单个分片
  • 后期扩容和迁移非常痛苦

七、How:怎么落地

7.1 常见落地方案

方案 做法 优点 缺点
业务代码路由 应用自己算库表并拼 SQL 灵活、依赖少 侵入业务、维护成本高
分库分表中间件 由中间件负责路由、改写、聚合 对业务更友好 增加中间层复杂度
数据库代理层 应用只连代理,代理再转发 业务侵入较小 代理成为关键组件

7.2 你至少要知道的中间件

  • ShardingSphere
  • MyCat

不用一上来就展开源码,但要知道它们在做什么:

  • SQL 路由
  • SQL 改写
  • 结果聚合
  • 分页处理
  • 一部分分布式事务支持

7.3 一个简单路由示例

1
2
int dbIndex = userId % 4;
int tableIndex = userId % 8;

这意味着:

  • 先用 userId 算出落在哪个库
  • 再用同一个或另一个规则算出落在哪张表
  • 查询时必须先定位分片,再执行 SQL

八、适用场景

场景 为什么适合
电商订单系统 写多、历史数据膨胀快、天然有用户/商户维度
支付流水、账单流水 插入量大、查询经常带账户和时间
SaaS 多租户系统 tenant_id 天然适合作为隔离维度
社交、消息、评论 数据量极大,常配合冷热分层和归档
日志、埋点、审计系统 时间特征强,非常适合按时间分片

九、收益与代价

9.1 收益

收益 说明
提升单表访问性能 单表变小,索引和扫描成本下降
提升整体吞吐能力 读写压力分散到多个节点
更容易横向扩展 可以通过增加节点来扩容
降低故障影响面 单分片故障不一定拖垮全站
更方便做冷热分离 归档、迁移、清理会更容易

9.2 代价

代价 说明
系统复杂度暴增 路由、聚合、治理、监控都更复杂
跨库跨表查询麻烦 不带分片键时常常需要广播查询
全局排序/分页困难 要先分片查询,再合并排序
聚合函数复杂 countsumavg 需要全局聚合
分布式事务复杂 跨库一致性不再由单库事务直接保证
主键生成复杂 需要全局唯一 ID
扩容迁移痛苦 分片规则一变,历史数据就要迁移
运维排障更难 热点、倾斜、路由和故障定位都更难

十、拆完之后最麻烦的 6 件事

10.1 跨库跨表查询

例如:

1
select * from orders where status = 1;

如果没有带分片键,就可能需要:

  1. 扫所有库
  2. 扫所有表
  3. 再做汇总

这就是广播查询,成本很高。

10.2 全局排序与分页

例如:

1
select * from orders order by create_time limit 20;

常见处理方式:

  1. 每个分片先查一部分
  2. 中间件或应用层做归并排序
  3. 再截取最终结果

如果是大偏移量分页,如 limit 1000,10,成本会更高。

10.3 聚合函数

像下面这些都无法直接在单个分片上得到全局结果:

1
2
3
count(*)
sum(amount)
avg(score)

通常做法是先分片计算,再全局聚合。

10.4 分布式事务

单库事务靠数据库可以保证 ACID,但跨库之后,事务就变成了分布式事务问题。

常见思路:

方案 特点
2PC 强一致,但性能差、锁时间长
TCC 业务侵入强,但控制力强
Saga 适合长事务编排
最终一致性 互联网系统最常见,通常配合 MQ 和补偿机制

10.5 全局唯一 ID

每个分片都自增会冲突,所以通常要引入全局 ID 方案:

方案 优点 缺点
UUID 简单、全局唯一 太长、随机、索引性能差
Snowflake 全局唯一、趋势递增、适合索引 依赖机器号和时钟治理
号段模式 性能好、实现稳定 依赖号段分配服务或数据库表

10.6 扩容与数据迁移

如果原来是:

1
id % 4

后来想改成:

1
id % 8

那大量历史数据都可能要迁移。

所以真正困难的往往不是“第一次拆”,而是“后面怎么平滑扩容”。

十一、典型问题与常见应对

11.1 非分片字段查询怎么办

例如按 user_id 分片,但用户经常按 order_no 查订单。

常见思路:

  • 建全局索引
  • 建映射表
  • order_no 纳入分片规则设计
  • 借助搜索引擎做辅助查询

11.2 热点和数据倾斜怎么办

常见表现:

  • 某个超级用户打爆单分片
  • 某个大商户流量异常集中
  • 某个时间段数据全部打到一个分片

常见应对:

  • 重新设计分片键
  • 对热点做单独拆分
  • 引入缓存和限流
  • 对时间分片配合冷热分层

11.3 跨库 Join 怎么办

常见做法:

做法 说明
中间件内存 Join 先查多份数据,再在中间件层拼装
广播表 小表复制到各个分片,改为本地 Join
反范式设计 冗余常用字段,尽量避免 Join

十二、三个例子帮助理解

12.1 例子一:按 user_id 分订单表

1
2
3
4
db_order_0.orders_0 ~ orders_7
db_order_1.orders_0 ~ orders_7
db_order_2.orders_0 ~ orders_7
db_order_3.orders_0 ~ orders_7

路由规则:

1
2
dbIndex = user_id % 4;
tableIndex = user_id % 8;

优点:

  • 查“某个用户的订单”很快
  • 数据分布通常比较均匀

问题:

  • 按订单状态查全站订单很麻烦
  • 后台全局分页很麻烦
  • 全平台订单统计也更复杂

12.2 例子二:按月份分流水表

1
2
3
trade_flow_202601
trade_flow_202602
trade_flow_202603

优点:

  • 插入和归档都方便
  • 历史清理简单

问题:

  • 跨月查询麻烦
  • 热点集中在最近月份

12.3 例子三:垂直拆分用户表

原表:

1
user(id, name, phone, avatar, intro, address, settings, ...)

拆成:

1
2
3
user_base
user_profile
user_setting

优点:

  • 常用字段更聚焦
  • 查询更轻

问题:

  • 查完整用户信息时可能要 Join 或多次查询

十三、复习 Checklist

13.1 基本概念

  • 什么是分库
  • 什么是分表
  • 什么是水平拆分
  • 什么是垂直拆分
  • 为什么拆
  • 什么时候拆

13.2 设计关键点

  • 如何选分片键
  • 常见分片算法有哪些
  • SQL 是怎么做路由的
  • 为什么需要全局唯一 ID
  • 为什么全局分页和聚合更难
  • 为什么会出现分布式事务问题

13.3 工程风险点

  • 数据倾斜
  • 热点问题
  • 非分片字段查询
  • 扩容迁移
  • 运维排障

十四、面试高频问答

14.1 分库分表解决了什么问题

  • 单机容量瓶颈
  • 单表性能瓶颈
  • 并发读写瓶颈
  • 可扩展性瓶颈

14.2 什么时候该分库分表

  • 常规优化做完后,单机仍是瓶颈
  • 业务增长持续且可预期
  • 业务有天然拆分维度

14.3 分片键怎么选

  • 查询高频使用
  • 数据分布均匀
  • 字段稳定
  • 避免热点
  • 便于未来扩容

14.4 分库分表的主要缺点

  • 跨库查询复杂
  • 排序、分页、聚合复杂
  • 分布式事务复杂
  • 扩容迁移复杂
  • 运维复杂

14.5 分库分表后主键怎么生成

  • 不能只依赖数据库自增
  • 常用 Snowflake 或号段模式生成全局唯一 ID

14.6 为什么不推荐一开始就分库分表

  • 复杂度高
  • 开发和运维成本高
  • 很多业务根本还没到那个量级
  • 低成本方案往往已经足够

十五、一段可直接复述的回答

分库分表是在单库单表的数据量、并发、容量和扩展性达到瓶颈后,对数据库进行拆分的一种架构手段。
从拆分方式看,可以分为垂直拆分和水平拆分;垂直拆分按业务或字段维度拆,水平拆分按数据行维度拆。
它的主要好处是降低单表数据量、分散单机压力、提升整体吞吐,并支持横向扩展。
但它也会引入很多复杂问题,比如分片键选择、跨库跨表查询、全局排序分页、分布式事务、全局主键生成以及后续扩容迁移。
所以分库分表本质上是用复杂度换规模能力,只有在单机数据库确实成为瓶颈、且业务具备清晰分片维度时,才值得采用。

十六、最后再压缩成一句话

[!tip] 最后记忆点
分库分表不是“把一个表拆成多个表”这么简单,它真正解决的是单机数据库的规模瓶颈,而真正增加的是整个系统的工程复杂度。