分库分表
分库分表
[!abstract] 一句话理解
分库分表是把原本集中在单机数据库上的数据和访问压力,按规则拆分到多个库、多个表中,以突破容量、性能和扩展性瓶颈。
一、先记住这 5 句话
- 分库分表解决的是单机数据库的容量、吞吐和扩展性问题。
- 它的本质是用系统复杂度换规模能力。
- 不是所有系统都需要分库分表,没有痛点不要硬拆。
- 分片键选得是否合理,基本决定方案成败。
- 真正麻烦的不是“怎么拆”,而是拆完之后怎么查、怎么排、怎么扩、怎么保证一致性。
二、5W 总览
| 维度 | 核心问题 | 一句话结论 |
|---|---|---|
| What | 什么是分库分表 | 按规则把数据拆到多个库和表 |
| Why | 为什么要做 | 单库单表扛不住数据量、吞吐和增长 |
| When | 什么时候做 | 常规优化做完后,单机仍是瓶颈再做 |
| Where | 按什么维度拆 | 关键在分片键(Sharding Key) |
| How | 怎么落地 | 代码路由、中间件、代理层都可以 |
三、What:什么是分库分表
3.1 四种常见拆分方式
| 方式 | 含义 | 示例 | 说明 |
|---|---|---|---|
| 水平分表 | 按数据行拆到多张表 | order -> order_0 ~ order_7 |
常用于单表过大 |
| 水平分库 | 按数据行拆到多个库 | db_order_0 ~ db_order_3 |
常用于单库容量或吞吐不足 |
| 垂直分表 | 按字段拆到不同表 | user_base、user_profile |
让高频字段和低频字段分离 |
| 垂直分库 | 按业务模块拆到不同库 | 用户库、订单库、支付库 | 本质上更接近按业务边界拆系统 |
3.2 一个典型例子
原来只有一张大订单表:
1 | orders |
拆分后可能变成:
1 | db_order_0.orders_0 ~ orders_7 |
这就是业界最常见的模式之一:先分库,再分表。
3.3 水平拆分和垂直拆分的区别
水平拆分
- 拆的是“数据行”
- 目的是分散数据量和访问压力
- 典型问题是路由、分页、聚合、事务、扩容
垂直拆分
- 拆的是“字段”或“业务模块”
- 目的是降低耦合、优化访问模式
- 典型问题是跨表查询和跨服务协同
示例:
1 | user(id, name, age, phone, avatar, intro, address, create_time) |
可以拆成:
1 | user_base(id, name, age, phone, create_time) |
核心原则:经常一起访问的字段放在一起。
四、Why:为什么要做分库分表
4.1 单库单表的典型瓶颈
| 问题 | 常见表现 |
|---|---|
| 数据量太大 | 索引树变高、扫描范围变大、DDL 变慢 |
| 写入压力太高 | 主库 QPS 到顶、IO 打满、锁竞争加重 |
| 读取压力太高 | 热点表读流量过于集中,主从也扛不住 |
| 单机容量有限 | CPU、内存、磁盘、网络带宽都有上限 |
| 运维风险上升 | 备份恢复慢、扩容难、故障影响范围大 |
4.2 本质结论
[!important] 本质
分库分表不是“白赚性能”,而是用更高的系统复杂度,换取更高的容量、吞吐和扩展性。
4.3 不要忽略前置优化
在考虑分库分表之前,通常应先尝试:
- SQL 优化
- 索引优化
- 读写分离
- 缓存
- 历史数据归档
- 垂直拆分
如果这些都做了,瓶颈依然明显,再考虑水平拆分。
五、When:什么时候该做
5.1 适合开始评估拆分的信号
- 单表数据量持续膨胀到几千万甚至上亿。
- 主库写入压力长期接近上限,高峰期明显堆积。
- DDL、备份、恢复、扩容越来越难做。
- 业务天然具备清晰的拆分维度,如用户、租户、商户、地域。
- 业务增长可预期,继续拖延只会让后续迁移更痛苦。
5.2 一个常见误区
[!warning] 不要一开始就拆
很多系统的问题,本质上还停留在 SQL、索引、缓存或冷热分离层面。能在低复杂度阶段解决,就不要过早引入分库分表。
六、Where:按什么维度拆
6.1 什么是分片键
分片键(Sharding Key)是决定一条数据落到哪个库、哪张表的关键字段。
常见分片维度:
user_idorder_idmerchant_idtenant_idregion_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 | int dbIndex = userId % 4; |
这意味着:
- 先用
userId算出落在哪个库 - 再用同一个或另一个规则算出落在哪张表
- 查询时必须先定位分片,再执行 SQL
八、适用场景
| 场景 | 为什么适合 |
|---|---|
| 电商订单系统 | 写多、历史数据膨胀快、天然有用户/商户维度 |
| 支付流水、账单流水 | 插入量大、查询经常带账户和时间 |
| SaaS 多租户系统 | tenant_id 天然适合作为隔离维度 |
| 社交、消息、评论 | 数据量极大,常配合冷热分层和归档 |
| 日志、埋点、审计系统 | 时间特征强,非常适合按时间分片 |
九、收益与代价
9.1 收益
| 收益 | 说明 |
|---|---|
| 提升单表访问性能 | 单表变小,索引和扫描成本下降 |
| 提升整体吞吐能力 | 读写压力分散到多个节点 |
| 更容易横向扩展 | 可以通过增加节点来扩容 |
| 降低故障影响面 | 单分片故障不一定拖垮全站 |
| 更方便做冷热分离 | 归档、迁移、清理会更容易 |
9.2 代价
| 代价 | 说明 |
|---|---|
| 系统复杂度暴增 | 路由、聚合、治理、监控都更复杂 |
| 跨库跨表查询麻烦 | 不带分片键时常常需要广播查询 |
| 全局排序/分页困难 | 要先分片查询,再合并排序 |
| 聚合函数复杂 | count、sum、avg 需要全局聚合 |
| 分布式事务复杂 | 跨库一致性不再由单库事务直接保证 |
| 主键生成复杂 | 需要全局唯一 ID |
| 扩容迁移痛苦 | 分片规则一变,历史数据就要迁移 |
| 运维排障更难 | 热点、倾斜、路由和故障定位都更难 |
十、拆完之后最麻烦的 6 件事
10.1 跨库跨表查询
例如:
1 | select * from orders where status = 1; |
如果没有带分片键,就可能需要:
- 扫所有库
- 扫所有表
- 再做汇总
这就是广播查询,成本很高。
10.2 全局排序与分页
例如:
1 | select * from orders order by create_time limit 20; |
常见处理方式:
- 每个分片先查一部分
- 中间件或应用层做归并排序
- 再截取最终结果
如果是大偏移量分页,如 limit 1000,10,成本会更高。
10.3 聚合函数
像下面这些都无法直接在单个分片上得到全局结果:
1 | count(*) |
通常做法是先分片计算,再全局聚合。
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 | db_order_0.orders_0 ~ orders_7 |
路由规则:
1 | dbIndex = user_id % 4; |
优点:
- 查“某个用户的订单”很快
- 数据分布通常比较均匀
问题:
- 按订单状态查全站订单很麻烦
- 后台全局分页很麻烦
- 全平台订单统计也更复杂
12.2 例子二:按月份分流水表
1 | trade_flow_202601 |
优点:
- 插入和归档都方便
- 历史清理简单
问题:
- 跨月查询麻烦
- 热点集中在最近月份
12.3 例子三:垂直拆分用户表
原表:
1 | user(id, name, phone, avatar, intro, address, settings, ...) |
拆成:
1 | user_base |
优点:
- 常用字段更聚焦
- 查询更轻
问题:
- 查完整用户信息时可能要 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] 最后记忆点
分库分表不是“把一个表拆成多个表”这么简单,它真正解决的是单机数据库的规模瓶颈,而真正增加的是整个系统的工程复杂度。