Skip to content

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 时考虑
单表文件大小超过 100GBB+ 树层级过深,查询 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)。

拆法:把"热字段"留在主表,"冷字段/大字段"拆到扩展表,通过主键一对一关联。

实战示例:商品表拆分

sql
-- 拆分前:一张 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 张结构相同的表。

sql
-- 拆分前
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 分片键的选择:一步错步步错 ​

分片键是决定拆分成败的最关键决策,选错了后期几乎无法回头。

三大铁律:

  1. 高频查询条件优先:80% 的 SQL 都带 WHERE user_id = ?,分片键就选 user_id
  2. 避免跨分片查询:如果经常要按非分片键查询(如按订单号查订单,但分片键是 user_id),要么忍受广播查询,要么建立"订单号 → user_id"的映射表(基因法)
  3. 数据均匀分布:分片键的取值要足够分散,别选一个 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
缺点:多一跳网络开销,代理层要做高可用

三、总结 ​

分库分表的核心逻辑就三句话:

  1. 为什么拆:单库单表的容量、性能、可用性都有天花板,拆是为了水平扩展
  2. 怎么拆:先垂直(按业务/字段拆,解决耦合),后水平(按数据行拆,解决容量);水平拆分的核心是选对分片键和分片算法
  3. 拆的代价:跨库 JOIN、分布式事务、全局 ID、扩容迁移——每一个都是要提前想清楚的问题

最后的忠告:

分库分表是"大杀器",不是"保健品"。 业务没到这个量级,盲目拆分只会带来巨大的复杂度; 业务真到了这个量级,就要果断拆、规范拆,别等问题爆发再动手。


文档版本:v1.0
适用版本:MySQL 5.7 / 8.0
最后更新:2024-03-15
延伸阅读:分库分表落地后,建议结合本系列的主从复制文档,为每个分片配置主从架构保障高可用