3739 字
19 分钟
MySQL 分库分表

分库分表是系统优化的”最后一招”——当单表数据突破千万、单机磁盘和并发到达瓶颈时才启用。垂直拆分按”列”切一刀,解决表太宽的问题,本质是冷热分离;水平拆分按”行”切 N 刀,解决表太长的瓶颈,本质是用空间和代码复杂度换单表性能。分库分表虽然解决了物理瓶颈,却将复杂性全部转移到了业务代码和分布式中间件上。


目录#

  1. 垂直拆分
  2. 水平拆分
  3. 分表后的三大痛点
  4. 常见面试题
  5. 总结

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/O4~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: 生成全局唯一 ID
userID := 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:

  1. 基因法:把查询条件关联到分片键。例如手机号注册时,将手机号与 user_id 的映射存入映射表,按手机号查时先走映射表。
  2. 异构索引:建立 ES / HBase 二级索引,非分片键的查询走 ES,拿到 ID 后回 MySQL 查。
  3. 广播查询:并发查所有分片后在内存中合并(只适合数据量小、并发低的场景,不推荐)。

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 8.0 / InnoDB 引擎编写,分库分表的设计原理(垂直/水平拆分、分布式 ID、路由策略)为架构层面通用知识,与具体数据库版本无关。

MySQL 分库分表
https://shirone.mysqil.com/posts/mysql/mysql-分库分表/
作者
HikariTish
发布于
2026-05-29
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 139 天

部分内容可能已过时