MySQL 分库分表实战指南:从单表瓶颈到分布式架构
当你的单表数据突破千万级、查询越来越慢、磁盘告警不断时,分库分表就是绕不开的方案。这篇文章用大白话讲清楚分库分表是什么、为什么需要它,以及到底该怎么拆。
目录
一、分库分表介绍
1.1 从一个真实的故事说起
某电商平台刚上线时,订单表 orders 每天新增几千条数据,查询飞快,谁也没在意。
两年后,业务爆发了:
- 订单表数据量突破 8000 万行,单表文件超过 100GB
- 用户查询"我的订单"列表,从 50ms 变成了 5 秒
- 每晚的数据备份从 10 分钟变成了 3 小时
- 想给订单表加个字段,
ALTER TABLE执行了一整夜还没完,业务被迫停机
DBA 尝试了所有常规手段:
- 加索引 → 有用,但索引文件本身也膨胀到了 30GB
- 升级硬件 → 从 16G 内存升到 128G,缓解了几个月又慢了
- 读写分离 → 从库也扛不住,因为单表太大,复制都开始延迟
问题的本质:单台 MySQL 的处理能力是有天花板的。当数据量和并发量超过这个天花板,任何优化都只是"续命",不是"治病"。
治病的药方就是——分库分表。
1.2 什么是分库分表
一句话定义:
分库分表就是把原来存储在一个数据库、一张表里的数据,按照某种规则拆开,分散到多个数据库、多张表中去存储和访问的技术方案。
用生活化的类比:
原来全公司 10 万份档案都堆在一个仓库的一个柜子里,找一份文件要翻半天。 现在把档案按部门拆到 10 个柜子(分表),再把柜子分散到 4 个仓库(分库), 找文件时先确定去哪个仓库、哪个柜子,一次就能拿到。
拆完之后的效果:
| 指标 | 拆分前 | 拆分后(8 库 × 8 表 = 64 张表) |
|---|---|---|
| 单表数据量 | 8000 万行 | 约 125 万行/表 |
| 单表文件大小 | 100GB | 约 1.6GB |
| 订单列表查询 | 5 秒 | 30ms |
| 加字段 DDL | 一整夜(还阻塞业务) | 单表几秒,可逐表执行 |
| 备份 | 单实例 3 小时 | 多实例并行,20 分钟 |
1.3 先搞清楚四个概念
很多人刚接触时容易混淆,先把术语说透:
┌─────────────────────────────────────────────────┐
│ 分库分表 │
├──────────────┬──────────────┬──────────────────┤
│ 垂直分表 │ 垂直分库 │ 水平分表/分库 │
│ (拆字段) │ (拆业务) │ (拆数据行) │
└──────────────┴──────────────┴──────────────────┘| 概念 | 拆的是什么 | 类比 |
|---|---|---|
| 垂直分表 | 把一张宽表的字段拆到多张表 | 把档案的"基本信息"和"附件材料"分两个夹子放 |
| 垂直分库 | 按业务模块把表拆到不同库 | 把财务部档案和人事部档案放不同仓库 |
| 水平分表 | 把一张表的数据行拆到同库多张表 | 一个柜子放不下,用 8 个一模一样的柜子放 |
| 水平分库 | 把数据行拆到不同库的多张表 | 柜子分散到多个仓库 |
重点:通常大家讨论的"分库分表"核心是指水平拆分——因为数据量爆炸才是最大的痛点。垂直拆分更多在业务架构演进时自然发生。
1.4 什么时候需要分库分表
不要一上来就分库分表! 它带来的复杂度远超你的想象。先看看这些"前置手段"是否都用过:
优化路线(按顺序尝试):
单表优化(索引、SQL 改写)
↓ 还不够
读写分离(主从架构)
↓ 还不够
缓存(Redis 挡在前面)
↓ 还不够
垂直拆分(按业务拆库)
↓ 还不够
水平拆分(分库分表) ← 最后的手段需要分库分表的信号:
| 信号 | 参考阈值 | 说明 |
|---|---|---|
| 单表数据量 | 超过 2000 万行 | 经验值,阿里规约建议 500 万行或 2GB 时考虑 |
| 单表文件大小 | 超过 100GB | B+ 树层级过深,查询 I/O 次数增加 |
| TPS/QPS | 单实例写 TPS 超过 5000 | 单机写入瓶颈 |
| 磁盘容量 | 单实例数据超过 1TB | 备份、恢复、迁移都成问题 |
| DDL 耗时 | 加字段超过 1 小时 | 影响业务发布节奏 |
经验之谈:这些阈值不是铁律。一台 128G 内存、NVMe SSD 的服务器,单表 5000 万行也能跑得不错。关键看业务增长趋势——如果增速快,提前规划;如果已经稳定,能不动就不动。
1.5 分库分表带来的挑战(先说清楚坑)
分库分表不是免费的午餐,拆分后要面对这些棘手问题:
| 挑战 | 具体问题 |
|---|---|
| 分布式事务 | 一次下单要扣库存、写订单,数据在不同库,怎么保证一致性? |
| 跨库 JOIN | 订单 JOIN 用户 原来一条 SQL,现在数据在两个库,JOIN 不了 |
| 跨库分页排序 | ORDER BY create_time LIMIT 100000, 20,每个分片都要查再合并 |
| 全局唯一 ID | 自增 ID 在多个库会冲突,需要雪花算法等分布式 ID 方案 |
| 扩容迁移 | 从 8 个库扩到 16 个库,数据怎么平滑迁移? |
| 运维复杂度 | 原来管 1 个实例,现在管 16 个实例,监控、备份、巡检全翻倍 |
所以记住一句话:
能用索引解决的别上缓存,能用缓存解决的别上主从,能用主从解决的别分库分表。分库分表是最后的选择,但一旦决定拆,就要拆得彻底、拆得规范。
二、分库分表的拆分方式
2.1 垂直拆分
垂直拆分的核心思想是"按业务边界拆",拆出来的结构各不相同。
2.1.1 垂直分表(拆字段)
场景:一张表字段特别多(几十上百个),有些字段访问频率极高(如商品标题、价格),有些字段很大但很少访问(如商品详情富文本、扩展 JSON)。
拆法:把"热字段"留在主表,"冷字段/大字段"拆到扩展表,通过主键一对一关联。
实战示例:商品表拆分
-- 拆分前:一张 60 个字段的大宽表
CREATE TABLE product (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200) NOT NULL, -- 高频访问
price DECIMAL(10,2) NOT NULL, -- 高频访问
stock INT NOT NULL, -- 高频访问
status TINYINT NOT NULL, -- 高频访问
-- ... 还有几十个字段
detail LONGTEXT, -- 商品详情 HTML,平均 50KB,很少查
description TEXT, -- 商品描述,列表页不展示
extend_json JSON, -- 扩展属性,偶尔用
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 拆分后:主表只留高频、小字段
CREATE TABLE product (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL,
status TINYINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 扩展表:存大字段、低频字段
CREATE TABLE product_ext (
id BIGINT PRIMARY KEY, -- 与主表 id 一致
detail LONGTEXT,
description TEXT,
extend_json JSON
);收益:
- 商品列表页只查主表,单行数据从 50KB+ 降到几百字节
- InnoDB 缓冲池能缓存更多行,缓存命中率大幅提升
- 主表行变窄后,B+ 树更"矮胖",查询 I/O 次数减少
拆分原则:
| 原则 | 说明 |
|---|---|
| 冷热分离 | 高频访问字段留主表,低频字段拆出去 |
| 大小分离 | TEXT/BLOB/JSON 大字段优先拆出 |
| 一一对应 | 扩展表与主表共用主键,保证一对一关联 |
2.1.2 垂直分库(拆业务)
场景:单体应用里所有业务表都在一个库,订单、用户、商品、营销互相耦合,一个慢 SQL 拖垮全站。
拆法:按业务模块把表划分到不同的数据库,每个库独立部署。
拆分前:单库 mall_db
├── user(用户相关表)
├── order(订单相关表)
├── product(商品相关表)
└── marketing(营销相关表)
拆分后:按业务独立成库
├── 用户库 user_db(192.168.1.11:3306)
│ └── user、user_address、user_level
├── 订单库 order_db(192.168.1.12:3306)
│ └── order、order_item、order_log
├── 商品库 product_db(192.168.1.13:3306)
│ └── product、product_ext、category
└── 营销库 marketing_db(192.168.1.14:3306)
└── coupon、activity、seckill收益:
- 业务解耦:订单库挂了不影响用户登录
- 资源隔离:大促时订单库压力大,可以单独扩容
- 团队自治:各业务团队独立管理自己的库,DDL 互不干扰
代价:
- 跨库 JOIN 没了:原来
SELECT * FROM order o JOIN user u ON ...,现在要在应用层分别查询再组装 - 本地事务没了:扣库存 + 写订单跨了库,需要分布式事务(Seata、消息队列最终一致性等)
2.2 水平拆分
水平拆分的核心思想是"同一个结构,按数据行拆",拆出来的表结构完全一样。
2.2.1 水平分表(同库拆表)
场景:单表数据量太大,但单实例的磁盘和连接数还扛得住。
拆法:在同一个库内,把一张表拆成 N 张结构相同的表。
-- 拆分前
orders(8000 万行)
-- 拆分后(按 user_id 取模拆 8 张表)
orders_0、orders_1、orders_2 ... orders_7(每张约 1000 万行)局限:所有分表还在同一个实例上,磁盘 I/O、连接数、CPU 还是共享的。它只解决了单表过大的问题,没解决单机瓶颈。
2.2.2 水平分库分表(跨库拆表)
场景:数据量和并发量都超过单机承载能力。
拆法:数据按规则分散到多个数据库实例,每个实例内再拆多张表。这是生产环境最常见的终极形态。
拆分前:
└── DB0.orders(8000 万行,单机)
拆分后:4 库 × 4 表 = 16 张分片表,每片约 500 万行
├── DB0(192.168.1.11)
│ ├── orders_0、orders_1、orders_2、orders_3
├── DB1(192.168.1.12)
│ ├── orders_0、orders_1、orders_2、orders_3
├── DB2(192.168.1.13)
│ ├── orders_0、orders_1、orders_2、orders_3
└── DB3(192.168.1.14)
├── orders_0、orders_1、orders_2、orders_3收益:容量、I/O、连接数、CPU 全部水平扩展,理论上可以无限加机器。
2.3 水平拆分的分片策略(核心中的核心)
水平拆分最关键的问题:一条数据到底该进哪个库、哪张表? 这个规则叫分片键(Sharding Key)+ 分片算法。
2.3.1 策略一:范围分片(Range)
规则:按分片键的取值范围划分。
用户表按用户 ID 范围拆分:
├── DB0.user:user_id 1 ~ 1000万
├── DB1.user:user_id 1000万 ~ 2000万
└── DB2.user:user_id 2000万 以上或按时间范围(订单表最常见):
订单表按月份拆分:
├── orders_202401:2024 年 1 月的订单
├── orders_202402:2024 年 2 月的订单
└── orders_202403:2024 年 3 月的订单优点:
- 扩容简单:满了就加新范围,历史数据不用动
- 范围查询友好:查"3 月份的订单"只打一个分片
缺点:
- 数据热点:最新分片承载所有写入和大部分查询,老分片没人访问,负载不均衡
适用场景:有明显时间属性的数据(订单、日志、流水),且查询主要按时间范围进行。
2.3.2 策略二:取模分片(Hash/Mod)
规则:对分片键取哈希后取模,均匀打散。
分片规则:分片编号 = user_id % 4
user_id = 1001 → 1001 % 4 = 1 → DB1
user_id = 1002 → 1002 % 4 = 2 → DB2
user_id = 1003 → 1003 % 4 = 3 → DB3
user_id = 1004 → 1004 % 4 = 0 → DB0优点:
- 数据均匀:每个分片数据量、访问量基本相当,没有热点
- 查询确定性:知道 user_id 就能直接算出分片,一次定位
缺点:
- 扩容痛苦:从 4 个库扩到 8 个库,
user_id % 8的结果全变了,约 75% 的数据要搬迁 - 范围查询不友好:查某个 ID 区间要打所有分片
扩容优化方案:
| 方案 | 思路 | 效果 |
|---|---|---|
| 翻倍扩容 | 4 → 8 → 16,每次翻倍 | 数据迁移量从 75% 降到 50% |
| 一致性哈希 | 哈希环上只影响相邻节点 | 迁移量降到 1/N |
| 预分片 | 一开始就按 1024 个虚拟分片设计,映射到少量物理库 | 扩容只改映射关系,不动数据 |
适用场景:用户维度的数据(用户信息、用户订单、用户积分),查询大多带分片键。
2.3.3 策略三:枚举/映射分片(Lookup)
规则:维护一张映射表,记录分片键到分片的对应关系。
映射表 shard_mapping:
┌──────────┬────────┐
│ user_id │ shard │
├──────────┼────────┤
│ 1001 │ DB0 │
│ 1002 │ DB2 │
│ 1003 │ DB1 │
└──────────┴────────┘
查询流程:先查映射表拿到 shard,再去对应库查数据优点:
- 灵活度最高:想怎么分就怎么分,支持个性化路由(如 VIP 用户进高性能库)
- 扩容平滑:只需修改映射表,配合数据迁移即可
缺点:
- 多一次映射查询(通常用 Redis 缓存映射关系解决)
- 映射表本身可能成为瓶颈和单点
适用场景:分片规则复杂、经常调整的业务。
2.3.4 三种策略对比总结
| 维度 | 范围分片 | 取模分片 | 枚举映射分片 |
|---|---|---|---|
| 数据均匀性 | 差(有热点) | 好 | 取决于配置 |
| 扩容难度 | 简单 | 困难(需迁移) | 简单 |
| 范围查询 | 友好 | 不友好 | 不友好 |
| 路由开销 | 零 | 零 | 一次映射查询 |
| 典型场景 | 时间序列数据 | 用户维度数据 | 复杂业务规则 |
2.4 分片键的选择:一步错步步错
分片键是决定拆分成败的最关键决策,选错了后期几乎无法回头。
三大铁律:
- 高频查询条件优先:80% 的 SQL 都带
WHERE user_id = ?,分片键就选user_id - 避免跨分片查询:如果经常要按非分片键查询(如按订单号查订单,但分片键是 user_id),要么忍受广播查询,要么建立"订单号 → user_id"的映射表(基因法)
- 数据均匀分布:分片键的取值要足够分散,别选一个 90% 取值都相同的字段
常见业务的分片键选择参考:
| 业务表 | 推荐分片键 | 理由 |
|---|---|---|
| 用户表 | user_id | 所有查询都围绕用户 |
| 订单表(C 端) | user_id | "我的订单"是最高频查询 |
| 订单表(B 端商家) | shop_id | 商家查自己的订单是高频场景 |
| 支付流水 | user_id 或 trade_no | 看主要查询维度 |
| 日志表 | 时间 | 按时间范围查询和清理 |
经典难题:订单表既要按 user_id 查,又要按 order_id 查怎么办?
基因法:生成订单号时,把 user_id 的"基因"(如末尾几位)嵌入订单号:
order_id = 时间戳(10位) + user_id后4位 + 随机数(4位)
查询时:从 order_id 提取 user_id 后4位 → 取模算出分片 → 直接定位这样无论按 user_id 还是 order_id 查询,都能路由到正确的分片,一次查询搞定。
2.5 垂直拆分 vs 水平拆分:怎么选
决策流程:
数据量大了、性能慢了
│
├─ 是某个业务模块特别重?
│ └─ 是 → 先做垂直分库,把重业务独立出去
│
├─ 是表太宽、大字段拖累?
│ └─ 是 → 先做垂直分表,冷热分离
│
└─ 单表行数/容量超标?
├─ 单机资源还够 → 水平分表(同库)
└─ 单机到瓶颈了 → 水平分库分表(跨实例)| 维度 | 垂直拆分 | 水平拆分 |
|---|---|---|
| 解决的问题 | 业务耦合、表结构臃肿 | 数据量和并发量瓶颈 |
| 拆分粒度 | 按业务/字段 | 按数据行 |
| 表结构 | 拆分后各不相同 | 拆分后完全一致 |
| 扩容能力 | 有限(业务就那么多) | 理论上无限 |
| 实施复杂度 | 较低 | 高(涉及路由、分布式事务) |
| 实施顺序 | 先垂直,后水平 | 垂直拆分不够用时再做 |
2.6 落地工具选型
分库分表不需要自己造轮子,主流中间件:
| 工具 | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| ShardingSphere-JDBC | 客户端 SDK | 无代理、性能高、Java 生态 | Java 应用首选 |
| ShardingSphere-Proxy | 代理层 | 对应用透明,支持多语言 | 异构技术栈 |
| MyCat | 代理层 | 老牌国产,文档丰富 | 中小团队 |
| Vitess | 代理层 | YouTube 出品,云原生 | 大规模集群 |
客户端模式 vs 代理模式:
客户端模式(ShardingSphere-JDBC):
应用 → [SDK 内置路由] → MySQL 各分片
优点:无中间层,性能高
缺点:与应用耦合,多语言支持差
代理模式(MyCat / ShardingSphere-Proxy):
应用 → [代理层路由] → MySQL 各分片
优点:应用无感知,像用单机 MySQL
缺点:多一跳网络开销,代理层要做高可用三、总结
分库分表的核心逻辑就三句话:
- 为什么拆:单库单表的容量、性能、可用性都有天花板,拆是为了水平扩展
- 怎么拆:先垂直(按业务/字段拆,解决耦合),后水平(按数据行拆,解决容量);水平拆分的核心是选对分片键和分片算法
- 拆的代价:跨库 JOIN、分布式事务、全局 ID、扩容迁移——每一个都是要提前想清楚的问题
最后的忠告:
分库分表是"大杀器",不是"保健品"。 业务没到这个量级,盲目拆分只会带来巨大的复杂度; 业务真到了这个量级,就要果断拆、规范拆,别等问题爆发再动手。
文档版本:v1.0
适用版本:MySQL 5.7 / 8.0
最后更新:2024-03-15
延伸阅读:分库分表落地后,建议结合本系列的主从复制文档,为每个分片配置主从架构保障高可用