我来详细讲解MySQL分库分表的策略、实现方式以及核心难点,这是高并发系统设计的必考点。
一、为什么需要分库分表
css
┌─────────────────────────────────────────┐
│ 单表瓶颈(InnoDB) │
│ - 数据量>5000万行,B+Tree高度增加,IO上升 │
│ - 单表文件>10GB,备份/DDL(加索引)耗时数小时 │
│ - 单库连接数上限(默认151,max_connections) │
│ - 写瓶颈:单机CPU/磁盘IO有上限 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 分库分表目标 │
│ 1. 分散存储压力(数据水平拆分) │
│ 2. 分散访问压力(读写流量分散到多实例) │
│ 3. 突破单机连接数限制 │
└─────────────────────────────────────────┘
二、分表 vs 分库 vs 分库分表
| 方案 | 定义 | 解决问题 | 复杂度 |
|---|---|---|---|
| 分表 | 单库内,表拆分为多张 | 单表数据量过大、索引膨胀 | 低 |
| 分库 | 数据分散到多个数据库实例 | 单库连接数瓶颈、写性能上限 | 中 |
| 分库分表 | 先分库,库内再分表 | 数据量+连接数双重瓶颈 | 高 |
三、分表策略(单库内)
1. 垂直分表(按列拆分)
bash
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ user表(宽表) │ │ user_basic(热数据) │
│ id, name, avatar, │ → │ id, name, avatar, phone │
│ phone, address, bio, │ │ (频繁查询,常驻内存) │
│ login_log, order_history │ └─────────────────────────────┘
│ (100+字段,行大小>4KB) │ ┌─────────────────────────────┐
└─────────────────────────────┘ │ user_extra(冷数据) │
│ id, address, bio, login_log │
│ (偶尔查询,可存SSD或归档) │
└─────────────────────────────┘
原则 :将访问频率不同 、数据大小差异大的字段分离。
- 热数据行小,Buffer Pool命中率高
- 冷数据可压缩存储,甚至归档到HBase/ClickHouse
2. 水平分表(按行拆分)
markdown
┌─────────────────────────────┐
│ user表(1000万行) │
└─────────────────────────────┘
↓ 按 user_id % 4
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│user_000│ │user_001│ │user_002│ │user_003│
│ 0-249w │ │250-499w│ │500-749w│ │750-999w│
└────────┘ └────────┘ └────────┘ └────────┘
分片键选择 :user_id(查询维度最频繁的字段)
四、分库策略(多实例)
1. 垂直分库(按业务域拆分)
scss
┌─────────────────────────────────────────┐
│ 单体数据库(所有业务) │
│ user | order | product | payment | log │
└─────────────────────────────────────────┘
↓
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ user-db │ │ order-db │ │ product-db │
│ (用户核心) │ │ (订单交易) │ │ (商品库存) │
│ 主从复制 │ │ 主从复制 │ │ 主从复制 │
└─────────────┘ └─────────────┘ └─────────────┘
特点:
- 业务解耦,独立扩容
- 跨库Join需应用层组装(或用宽表/ES冗余)
- 分布式事务问题(后文详述)
2. 水平分库(同业务数据分散)
scss
┌─────────────────────────────────────────┐
│ order-db(单机瓶颈) │
│ 日增100万订单,写入压力过大 │
└─────────────────────────────────────────┘
↓ 按 user_id % 4
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ order-db-00 │ │ order-db-01 │ │ order-db-02 │ │ order-db-03 │
│ user_id%4=0│ │ user_id%4=1│ │ user_id%4=2│ │ user_id%4=3│
│ (0,4,8...) │ │ (1,5,9...) │ │ (2,6,10..) │ │ (3,7,11..) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
五、分片算法(Sharding Algorithm)
1. 取模/哈希分片
java
// 简单取模
int dbIndex = userId % 4; // 分4库
int tableIndex = (userId / 4) % 8; // 每库8表,共32张表
// 一致性哈希(解决扩容时数据迁移问题)
// 节点增减只影响相邻区间,无需全量迁移
优点 :数据分布均匀,点查高效
缺点:扩容时数据迁移量大(需rehash),范围查询需扫所有分片
2. 范围分片
java
// 按时间范围(适合时序数据)
db_2024_q1: 2024-01-01 ~ 2024-03-31
db_2024_q2: 2024-04-01 ~ 2024-06-30
// 按ID范围
db_0: user_id 0 ~ 1000万
db_1: user_id 1000万 ~ 2000万
优点 :扩容简单(新增区间即可),范围查询友好
缺点:热点问题(新数据集中在最新分片),需配合读写分离
3. 混合分片(推荐)
java
// 先范围确定库,再取模确定表
// 避免取模扩容迁移,又避免范围热点
// 步骤1:按时间范围选库(如2024年数据在db_2024)
String db = "db_" + year;
// 步骤2:库内按user_id % 1024 分表
int table = (userId % 1024);
适用:日志、订单等时间敏感型数据。
4. 分片算法对比
| 算法 | 数据分布 | 扩容迁移 | 范围查询 | 适用场景 |
|---|---|---|---|---|
| 取模 | 均匀 | 大(全量rehash) | 差 | 用户ID等离散键 |
| 范围 | 可能不均 | 无(新增区间) | 优 | 时间序列、日志 |
| 一致性哈希 | 较均匀 | 小(相邻节点) | 差 | 缓存分片(Redis) |
| 混合 | 可控 | 中等 | 中等 | 订单、交易流水 |
六、分库分表实现方式
方式1:客户端Sharding(应用层)
java
// 使用ShardingSphere-JDBC(轻量,无中间件依赖)
@Configuration
public class ShardingConfig {
@Bean
public DataSource shardingDataSource() throws SQLException {
// 配置分片规则
ShardingRuleConfiguration rule = new ShardingRuleConfiguration();
// 分库策略:user_id取模
rule.setDefaultDatabaseShardingStrategy(
new StandardShardingStrategyConfiguration(
"user_id",
new InlineShardingStrategyConfiguration("ds_${user_id % 4}")
)
);
// 分表策略:每库8表
rule.setDefaultTableShardingStrategy(
new StandardShardingStrategyConfiguration(
"user_id",
new InlineShardingStrategyConfiguration("user_${user_id % 32}")
)
);
return ShardingSphereDataSourceFactory.createDataSource(
createDataSourceMap(),
Collections.singleton(rule),
new Properties()
);
}
}
优点 :性能损耗低(直接路由到真实数据源),无单点
缺点:配置侵入代码,语言绑定(Java)
方式2:代理中间件(Proxy)
scss
┌─────────┐ ┌─────────────┐ ┌─────────┐ ┌─────────┐
│ 应用 │────→│ Sharding │────→│ MySQL-0 │ │ MySQL-1 │
│ (任意语言)│ │ -Proxy │ │ 主从 │ │ 主从 │
└─────────┘ │ (独立进程) │ └─────────┘ └─────────┘
└─────────────┘
自动解析SQL,路由改写
优点 :对应用透明,语言无关,集中管控
缺点:多一层网络跳转(延迟+~1ms),Proxy本身高可用需保障
代表产品:ShardingSphere-Proxy、MyCat、Vitess
方式3:云原生/数据库中间件(NewSQL)
sql
-- TiDB/OceanBase 自动分片,应用无感知
CREATE TABLE user (
id BIGINT PRIMARY KEY,
name VARCHAR(50)
) ENGINE=InnoDB
PARTITION BY HASH(id) PARTITIONS 16; -- 自动分布式
-- 应用像访问单机MySQL一样访问TiDB
优点 :完全透明,自动扩缩容,强一致分布式事务
缺点:成熟度、生态、运维复杂度
七、核心难点与解决方案
难点1:分布式主键(全局唯一ID)
java
// 方案1:雪花算法(Snowflake)- 推荐
// 41位时间戳 + 10位机器ID + 12位序列号 = 64位Long
// 优点:趋势递增,插入性能高(避免B+Tree频繁分裂)
// 缺点:依赖时钟,时钟回拨会重复(需NTP校准或等待)
// 方案2:号段模式(Leaf)
// 从DB批量获取ID区间(如[1000,2000)),内存分配
// 优点:性能极高,无时钟依赖
// 缺点:需额外服务(美团Leaf)
// 方案3:数据库自增 + 步长
// DB-0: 1, 5, 9... (step=4, offset=1)
// DB-1: 2, 6, 10... (step=4, offset=2)
// 缺点:扩展困难,非连续
难点2:跨分片查询/聚合
sql
-- 场景:查询所有用户的订单总额(需聚合4库8表共32张表)
SELECT user_id, SUM(amount) FROM order GROUP BY user_id;
-- 方案1:应用层聚合(ShardingSphere自动处理)
// Proxy层并行查询各分片,内存归并结果
// 缺点:大数据量时内存压力大,延迟高
-- 方案2:异构索引表(冗余存储)
// 将聚合结果实时同步到ClickHouse/ES
// 查询走OLAP引擎,原始数据走MySQL分片
-- 方案3:限制查询维度
// 强制带分片键:WHERE user_id = ?(点查)
// 禁止无分片键的全局扫描(或走离线数仓)
难点3:分布式事务
java
// 场景:下单扣库存,跨order-db和inventory-db
// 方案1:最终一致性(Saga/TCC)- 互联网主流
@Compensable(confirmMethod = "confirm", cancelMethod = "cancel")
public void createOrder(Order order) {
orderService.save(order); // 本地事务
inventoryService.deduct(order); // RPC调用,可能失败
}
// 失败时执行cancel:回滚订单 + 释放库存
// 方案2:Seata AT模式(自动补偿)
// 代理数据源,解析SQL生成Undo Log,全局协调器驱动二阶段提交
// 方案3:XA协议(强一致,性能差)
// 两阶段提交,阻塞协议,很少用于高并发场景
难点4:数据迁移与扩容
markdown
平滑扩容流程(取模→取模,如4库扩8库):
1. 双写阶段(2周)
应用层同时写旧分片(4库)和新分片(8库)
读走旧分片(保证一致性)
2. 历史数据迁移
脚本将旧4库数据rehash到新8库
对比校验(MD5/行数)
3. 切读阶段
灰度将读流量切到新8库
观察一周,比对数据一致性
4. 停写旧库
关闭双写,只写新8库
保留旧库备份,1月后清理
八、分库分表设计 checklist
yaml
1. 是否真的需要?
- 数据量<1000万?先优化索引/SQL,加缓存,读写分离
- QPS<1万?单机+主从足够
2. 分片键选择
- 查询维度最多的字段(如user_id)
- 避免跨分片查询(如按user_id分片,但大量查询按time_range)
3. 分几片?
- 预估3-5年数据量,取模数选2^n(方便后续分裂)
- 示例:当前500万/年,预期5年2500万,分32片(每片~80万,健康)
4. 全局表(小数据量配置表)
- 每个库冗余一份,避免跨库Join
- 或放分布式缓存(Redis)
5. 非分片键查询
- 建立异构索引表(如按手机号查询,需额外维护mobile→user_id映射)
- 或同步到Elasticsearch
九、高频面试追问
Q:分库分表后如何分页查询?
sql
-- 场景:ORDER BY time LIMIT 100000, 10(深分页)
-- 问题:需每个分片查100010条,内存归并排序,性能极差
-- 方案1:禁止深分页,产品层限制(只能前100页)
-- 方案2:游标分页(上次最后一条的time作为起点)
WHERE time < '2024-01-01 12:00:00' ORDER BY time DESC LIMIT 10;
-- 方案3:聚合到ES/ClickHouse,分库分表只支持点查
Q:分库分表后Join怎么办?
java
// 同库Join:ShardingSphere支持(如果关联表分片策略相同)
// 跨库Join:应用层组装,或宽表冗余
// 反范式设计(空间换时间)
// 订单表冗余用户姓名、商品名称,避免Join
CREATE TABLE order (
order_id BIGINT,
user_id BIGINT,
user_name VARCHAR(50), -- 冗余,避免查user表
product_id BIGINT,
product_name VARCHAR(100), -- 冗余
...
);
Q:分库分表和NewSQL(TiDB)怎么选?
| 维度 | 分库分表+Proxy | NewSQL(TiDB) |
|---|---|---|
| 成熟度 | 高,生态完善 | 中,快速迭代 |
| 运维成本 | 高(自研Proxy、扩容脚本) | 低(自动化) |
| 性能 | 接近原生MySQL | ~70%原生MySQL(分布式事务开销) |
| 复杂查询 | 受限(跨分片性能差) | 友好(自动分布式执行计划) |
| 团队规模 | 需DBA+中间件专家 | 可少配DBA |
建议:
- 已有成熟MySQL生态,团队强 → 分库分表
- 新业务,团队小,想快速上线 → TiDB/OceanBase
分库分表是**"最后一招"**,在此之前应穷尽优化手段(索引、缓存、读写分离、归档)。一旦拆分,架构复杂度不可逆上升,需配套完善的运维体系和监控能力。