分库分表是系统优化的”最后一招”——当单表数据突破千万、单机磁盘和并发到达瓶颈时才启用。垂直拆分按”列”切一刀,解决表太宽的问题,本质是冷热分离;水平拆分按”行”切 N 刀,解决表太长的瓶颈,本质是用空间和代码复杂度换单表性能。分库分表虽然解决了物理瓶颈,却将复杂性全部转移到了业务代码和分布式中间件上。
目录
1. 垂直拆分
垂直拆分的本质是**“专库专用”或”冷热分离”**,它不改变数据的行数,而是改变表的结构——按列或业务领域切一刀。
1.1 垂直分库(微服务化)
业务早期,所有数据都塞在一个库里。当业务做大后,按照业务领域将表拆分到不同的数据库实例中:
┌─────────────────────────────────────────────────────────────┐│ 单体数据库(拆分前) ││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││ │ user │ │ order │ │ product │ │ payment │ ││ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │└──────────────────────┬──────────────────────────────────────┘ │ 垂直分库 ┌───────────────┼───────────────┐ ▼ ▼ ▼┌──────────────┐ ┌──────────────┐ ┌──────────────┐│ 用户库 │ │ 订单库 │ │ 商品库 ││ ┌──────────┐│ │ ┌──────────┐│ │ ┌──────────┐││ │user_base ││ │ │ orders ││ │ │ product │││ │user_ext ││ │ │order_item││ │ │ stock │││ └──────────┘│ │ └──────────┘│ │ └──────────┘│└──────────────┘ └──────────────┘ └──────────────┘核心收益:每个微服务独立拥有自己的数据库,互不影响。用户服务的数据库挂了不会影响订单查询;商品库的磁盘满了不影响用户注册。同时各库可以独立扩缩容、独立选型(甚至可以用不同存储引擎)。
1.2 垂直分表(大表瘦身)
当一张表有上百个字段,其中包含 个人详细简介、大头像 Base64 等极长且不常查询的字段时,需要将表按列拆分成主表和扩展表:
拆分前 拆分后┌─────────────────────────┐ ┌─────────────────┐ ┌─────────────────┐│ user │ │ user_base │ │ user_ext │├─────────────────────────┤ ├─────────────────┤ ├─────────────────┤│ id (高频, 短) │ ──▶ │ id │ │ id ││ name (高频, 短) │ ──▶ │ name │ │ 个人简介 (TEXT) ││ age (高频, 短) │ ──▶ │ age │ │ 大头像 (LONGTEXT)││ phone (高频, 短) │ ──▶ │ phone │ │ 扩展字段3 ││ 个人简介 (低频, 长) │ ──▶ │ ... │ │ ... ││ 大头像 (低频, 长) │ └─────────────────┘ └─────────────────┘│ ...共 100+ 字段 │└─────────────────────────┘为什么这样做?——Buffer Pool 视角的关键收益:
| 维度 | 拆分前 | 拆分后 |
|---|---|---|
| 单行大小 | 可能数 KB(含长字段) | user_base 单行仅数百字节 |
| Buffer Pool 单页可缓存行数 | 极少(可能只有几行) | 极多(可缓存数百行) |
| 高频查询命中率 | 低(大字段挤占内存) | 高(热数据集中于小表) |
| 低频查询影响 | 连带污染缓冲池 | 隔离在 user_ext,不影响热点 |
关键认知:垂直分表的优化目标不是减少磁盘空间,而是提升 Buffer Pool 的内存利用效率——让缓存命中更多热点行。
2. 水平拆分
水平拆分的本质是解决单表数据量过大的绝对性能瓶颈。
当 user 表数据量飙升到 5000 万行时,B+ 树的层级会变高(可能从 3 层变成 4 层甚至 5 层),每多一层就多一次磁盘 I/O,查询效率直线下滑。
核心机制:按分片键路由
按照某个分片规则(如哈希取模:user_id % 4),将一张大表拆分成表结构完全相同的 N 张子表:
拆分前 拆分后┌─────────────────────────┐ ┌──────────────────────────────┐│ user │ │ 按 user_id % 4 哈希路由 ││ 5000万 行 │ │ ││ B+ 树 4~5 层 │ ──▶ │ ┌────────┐ ┌────────┐ ││ 单表扛所有查询 │ │ │ user_0 │ │ user_1 │ │└─────────────────────────┘ │ │1250万 │ │1250万 │ │ │ └────────┘ └────────┘ │ │ ┌────────┐ ┌────────┐ │ │ │ user_2 │ │ user_3 │ │ │ │1250万 │ │1250万 │ │ │ └────────┘ └────────┘ │ │ 单表 B+ 树回到 3 层 │ └──────────────────────────────┘效果对比:
| 维度 | 拆分前 | 拆分后 |
|---|---|---|
| 单表数据量 | 5000 万 | 1250 万 × 4 |
| B+ 树层级 | 4~5 层 | 3 层 |
| 单次查询磁盘 I/O | 4~5 次 | 3 次 |
| 单表写入压力 | 全部写一张表 | 均分到 4 张表 |
一句话:B+ 树重新变得”矮胖”,查询性能恢复。
3. 分表后的三大痛点
水平分表虽然解决了单表容量和性能的物理瓶颈,但它将原本由单机数据库承担的复杂性,全部转移到了业务代码上。以下是三个最痛的噩梦:
3.1 全局唯一 ID 生成
| 单表时代 | 分表之后 | |
|---|---|---|
| 做法 | AUTO_INCREMENT 自增主键,一行 SQL 搞定 | 四张表各自自增,会出现 4 个 ID = 1 |
| 后果 | 系统全局唯一,永远不重复 | 主键冲突,数据彻底乱套 |
单表 AUTO_INCREMENT: 分表各自 AUTO_INCREMENT: user (单表) user_0 → ID: 1, 2, 3... ┌────┬──────┐ user_1 → ID: 1, 2, 3... ← 冲突! │ id │ name │ user_2 → ID: 1, 2, 3... ← 冲突! ├────┼──────┤ user_3 → ID: 1, 2, 3... ← 冲突! │ 1 │ 张三 │ │ 2 │ 李四 │ │ 3 │ 王五 │ ← 全局唯一 └────┴──────┘破局方案:
业务代码在执行 INSERT 之前,必须由 Go 后端通过算法生成全局唯一 ID。业界经典方案:
| 方案 | 原理 | 特点 |
|---|---|---|
| Snowflake(雪花算法) | 64 bit:1 位符号 + 41 位时间戳 + 10 位机器 ID + 12 位序列号 | Twitter 出品,高性能、趋势递增 |
| 美团 Leaf | 号段模式 + Snowflake 双模式 | 国内大厂广泛使用,高可用 |
| 数据库号段 | 批量从 DB 号段表取 ID 区间,用完再取 | 简单但依赖 DB |
-- 分表后的 INSERT(伪代码,Go 业务层)-- Step 1: 生成全局唯一 IDuserID := snowflake.Generate() // 例如: 8923472348923
-- Step 2: 计算路由tableIndex := userID % 4 // 确定落到 user_0 ~ user_3
-- Step 3: 写入对应子表INSERT INTO user_${tableIndex} (id, name, ...) VALUES (userID, '张三', ...)3.2 数据路由与查询维度
| 单表时代 | 分表之后 | |
|---|---|---|
| 做法 | SELECT * FROM user WHERE phone = '138...' | 需要知道分片键才能定位表 |
| 后果 | 一行代码直接搞定 | 如果查询条件不含分片键 → 必须广播查询所有子表 |
以 user_id 为分片键,但按 phone 查询:
查询: WHERE phone = '138xxxx' │ ├──→ user_0 ?不知道在不在 → 查! ├──→ user_1 ?不知道在不在 → 查! ├──→ user_2 ?不知道在不在 → 查! └──→ user_3 ?不知道在不在 → 查!
一次查询变成 4 次查询,然后合并结果 → 性能灾难!破局方案:
| 方案 | 原理 | 优缺点 |
|---|---|---|
| 映射表 | 建一张极小的表:phone → user_id,先查映射表拿到 user_id,再用分片键定位 | 简单,但多一次查询 |
| ES / MongoDB 同步 | 数据全量同步到 ES,非分片键查询走 ES,拿到 user_id 后再回 MySQL 取完整数据 | 复杂但强大,大厂标配 |
| 基因法 | 将用户属性编码进 ID(如 ID 后几位含地区码),使得某些查询可以”猜”出路由 | 侵入性强,适合特定场景 |
3.3 跨表关联 JOIN 与深度分页
| 单表时代 | 分表之后 | |
|---|---|---|
| 做法 | ORDER BY create_time DESC LIMIT 0, 10 | 需要合并多表结果后重新排序 |
| 后果 | 一行代码搞定 | Go 代码/中间件必须做内存归并排序 |
深度分页问题——取全局 TOP 10:
LIMIT 0,10 │ ├──→ user_0: 取 Top 10 ──→ [id:1003, 1001, 998...] ├──→ user_1: 取 Top 10 ──→ [id:1004, 1000, 997...] ├──→ user_2: 取 Top 10 ──→ [id:1002, 999, 995...] └──→ user_3: 取 Top 10 ──→ [id:1005, 996, 994...] │ 共 40 条数据 ──→ 内存归并大排序 ──→ 截取前 10 │ 内存开销 O(N × 表数),复杂度 O(N×logN)更糟糕的是偏移量大了之后:
LIMIT 1000000, 10
每张子表需要先取出前 1,000,010 条,共 4,000,040 条数据 → 内存归并这就是"分表后分页越往后越慢"的根源!破局方案:
| 方案 | 适用场景 | 原理 |
|---|---|---|
| 禁用跳页 | App 瀑布流 / 时间线 | 改用”上一页最后一条 ID”的游标分页 |
| 二次查询法 | 必须按时间排序 | 先用小偏移量从子表取,拿到时间边界后再精准查 |
| ES 承担排序 | 复杂搜索场景 | 排序和分页在 ES 完成,只回 MySQL 取最终数据 |
4. 常见面试题
Q1: 什么时候用垂直拆分,什么时候用水平拆分?
A1:
| 拆分类别 | 触发条件 | 解决什么问题 | 核心手段 |
|---|---|---|---|
| 垂直分表 | 表字段过多(100+),含大字段 | 单行太大 → Buffer Pool 缓存效率低 | 拆主表 + 扩展表,冷热分离 |
| 垂直分库 | 业务耦合严重,不同服务共用一个库 | 单库扛不住多业务流量 | 按业务域拆分独立数据库实例 |
| 水平分表 | 单表数据量突破千万级别 | B+ 树过深 → 磁盘 I/O 变多,写入性能下降 | 按分片键哈希路由到 N 张结构相同的子表 |
面试要点:垂直分表的目标是提升 Buffer Pool 命中率,水平分表的目标是降低 B+ 树高度、分散写入压力。两者解决的问题维度完全不同。
Q2: 水平分表后,为什么不能用数据库自增主键了?
A2:因为拆成多张表后,每张表各自维护自增计数器,会在不同表中生成相同的 ID,导致主键冲突。必须引入分布式 ID 生成器(如 Snowflake),在业务代码层生成全局唯一 ID,然后根据 ID 计算分片路由(如 id % N),再写入对应子表。
Q3: 为什么要尽量避免分库分表?
A3:分库分表带来了三个巨大成本——
| 成本 | 具体表现 |
|---|---|
| 代码复杂度爆炸 | 全局 ID 生成、分片路由、结果合并——原本一行 SQL 的事变成几十行业务代码 |
| 运维复杂度暴增 | DDL 变更要同步到所有子表、扩容需数据迁移、分布式事务协调 |
| 查询能力退化 | JOIN 基本报废、GROUP BY / ORDER BY 需要内存归并、非分片键查询要广播 |
优先级排序:SQL 优化 → 缓存(Redis)→ 读写分离 → 垂直拆分 → 最后才是水平分表。
Q4: 非分片键查询有什么解决思路?
A4:
- 基因法:把查询条件关联到分片键。例如手机号注册时,将手机号与 user_id 的映射存入映射表,按手机号查时先走映射表。
- 异构索引:建立 ES / HBase 二级索引,非分片键的查询走 ES,拿到 ID 后回 MySQL 查。
- 广播查询:并发查所有分片后在内存中合并(只适合数据量小、并发低的场景,不推荐)。
5. 总结
5.1 全景架构图
┌─────────────────────────────────────────────────────────────────────────────┐│ MySQL 分库分表全景 ││ ││ 拆分策略 ││ ┌─────────────────────────────────────────────────────────────────┐ ││ │ │ ││ │ ┌──────────────────────────┐ ┌──────────────────────────┐ │ ││ │ │ 垂直拆分 (按列) │ │ 水平拆分 (按行) │ │ ││ │ │ │ │ │ │ ││ │ │ 分库:按业务域拆数据库 │ │ 核心:user_id % N 路由 │ │ ││ │ │ user/order/product │ │ user_0 ~ user_3 │ │ ││ │ │ │ │ │ │ ││ │ │ 分表:冷热字段分离 │ │ 目标:降低 B+ 树高度 │ │ ││ │ │ user_base + user_ext │ │ 分摊写入压力 │ │ ││ │ │ │ │ │ │ ││ │ │ 目标:提升 Buffer Pool │ │ │ │ ││ │ │ 内存利用效率 │ │ │ │ ││ │ └──────────────────────────┘ └──────────────────────────┘ │ ││ └─────────────────────────────────────────────────────────────────┘ ││ ││ 分表后三大应对方案 ││ ┌─────────────────────────────────────────────────────────────────┐ ││ │ │ ││ │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │ ││ │ │ 全局唯一 ID │ │ 非分片键查询 │ │ 跨表 JOIN 分页│ │ ││ │ │ │ │ │ │ │ │ ││ │ │ Snowflake 雪花 │ │ 映射表 / ES 索引 │ │ 游标分页 │ │ ││ │ │ 美团 Leaf │ │ 基因法嵌入 ID │ │ ES 承担排序 │ │ ││ │ │ 数据库号段 │ │ 广播查询(不推荐) │ │ 内存归并 │ │ ││ │ └──────────────────┘ └──────────────────┘ └──────────────┘ │ ││ └─────────────────────────────────────────────────────────────────┘ ││ ││ 慎重!优化优先级 ││ ┌─────────────────────────────────────────────────────────────────┐ ││ │ SQL优化 → 缓存 → 读写分离 → 垂直拆分 → 水平分表(最后一招) │ ││ └─────────────────────────────────────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────────────────────┘5.2 关键设计原则
| 原则 | 说明 |
|---|---|
| 垂直分表不是省磁盘,是省内存 | 冷字段隔离,Buffer Pool 单页缓存更多热数据行 |
| 水平分表是用架构复杂度换物理性能 | B+ 树变矮 → I/O 减少,但代码和运维成本急剧上升 |
| 分片键是第一等公民 | 所有能走分片键的查询收益巨大,不走分片键的查询代价巨大 |
| ID 生成前移到应用层 | 不再依赖 AUTO_INCREMENT,必须先有 ID 才能确定路由 |
| 分库分表是最后一招 | 先尝试索引优化、缓存、读写分离,走投无路再上分库分表 |
| 优先使用 ES 等异构索引 | 弥补分表后查询能力的退化——搜索走 ES,明细走 MySQL |
5.3 快速排查清单
| 排查点 | 看什么 | 决策动作 |
|---|---|---|
| 单表数据量 | 是否突破 1000W | 先检查索引和慢 SQL,未必需要分表 |
| 单表字段数 | 是否超过 50~80 且含大字段 | 优先考虑垂直分表 |
| 业务耦合度 | 不同服务是否共库 | 优先垂直分库 → 微服务化 |
| Buffer Pool 命中率 | innodb_buffer_pool_read_requests / innodb_buffer_pool_reads | 命中率 < 95% 先考虑垂直分表提升内存效率 |
| 写入瓶颈 | 磁盘 IOPS 是否打满 | 读写分离 → 水平分表分散写入 |
| 分片键设计 | 业务最频繁的查询走什么字段 | 选高频查询字段做分片键,避免非分片键查询 |
5.4 一句口诀
垂直瘦身省内存,水平分片降树高;ID 前置免冲突,分片键外查全扫;分库分表最后招,索引缓存先做好。
相关链接
- MySQL 索引底层与优化 — B+ 树原理与索引优化,水平分表性能改善的理论基础
- MySQL Buffer Pool 与 Change Buffer — 垂直分表冷热分离的内存收益来源
- MySQL 事务、MVCC 与锁 — 分库分表后的分布式事务挑战
- MySQL 主从架构 — 读写分离,分库分表之前的优先选择
- Twitter Snowflake 原理解析
- 美团 Leaf 分布式 ID 生成系统
本文基于 MySQL 8.0 / InnoDB 引擎编写,分库分表的设计原理(垂直/水平拆分、分布式 ID、路由策略)为架构层面通用知识,与具体数据库版本无关。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 139 天
部分内容可能已过时