分库分表
问题
什么时候需要分库分表?垂直拆分和水平拆分有什么区别?常用的分片策略有哪些?分库分表后如何解决分布式 ID、跨库查询等问题?
答案
什么时候需要分库分表
| 问题 | 表现 | 解决方案 |
|---|---|---|
| 单表数据量过大 | 单表超过 2000 万行,查询变慢 | 水平分表 |
| 数据库连接数不够 | 高并发时连接池耗尽 | 分库 |
| 磁盘 I/O 瓶颈 | 磁盘读写满载 | 分库(分散 I/O) |
| 单库数据量过大 | 备份恢复时间过长 | 分库 |
分库分表是最后手段
分库分表会引入大量复杂性,在此之前应先尝试:
- SQL 优化 + 索引优化
- 读写分离(主写从读)
- 缓存(Redis 热点数据)
- 归档历史数据(冷热分离)
- 升级硬件(SSD、增加内存)
只有以上方案都无法满足需求时才考虑分库分表。
拆分方式
垂直拆分
| 类型 | 说明 | 适用场景 |
|---|---|---|
| 垂直分库 | 按业务模块拆分到不同数据库 | 微服务架构,各服务独立数据库 |
| 垂直分表 | 将大表按字段拆分(常用字段和不常用字段分开) | 表字段多且部分字段(如 TEXT/BLOB)很少查询 |
水平拆分
| 类型 | 说明 | 适用场景 |
|---|---|---|
| 水平分表 | 同一张表的数据按规则拆分到同库的多张表中 | 单表数据量过大 |
| 水平分库分表 | 数据同时分到不同库的不同表中 | 单表 + 单库都无法承受 |
分片策略
| 策略 | 规则 | 优点 | 缺点 |
|---|---|---|---|
| 取模 | hash(sharding_key) % N | 数据分布均匀 | 扩容需要迁移大量数据 |
| 范围 | 按 ID 或时间范围 | 扩容方便,只需新增分片 | 可能数据倾斜(新数据集中在新分片) |
| 一致性哈希 | 环形哈希 | 扩容只迁移部分数据 | 实现复杂 |
| 查表法 | 维护路由表 | 灵活 | 路由表本身可能成为瓶颈 |
取模分片示例
// 分 4 张表:orders_0, orders_1, orders_2, orders_3
int tableIndex = Math.abs(userId.hashCode()) % 4;
String tableName = "orders_" + tableIndex;
范围分片示例
// 按时间范围:2024 年的数据放 orders_2024
String tableName = "orders_" + year;
// 按 ID 范围:每 1000 万一张表
int tableIndex = (int)(id / 10000000);
String tableName = "orders_" + tableIndex;
分片键选择原则
- 选择查询频率最高的字段作为分片键(如按 user_id 分片,大部分查询都按 user_id)
- 数据分布应尽量均匀
- 避免跨分片查询(大部分查询应命中单个分片)
分库分表引入的问题
1. 分布式 ID
分库分表后不能使用数据库自增 ID(多个库独立自增会冲突)。
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| UUID | 128 位随机字符串 | 简单,无中心化 | 无序(影响 B+ 树插入)、长度大 |
| Snowflake 雪花算法 | 时间戳 + 机器 ID + 序列号 | 有序、高性能 | 依赖时钟,时钟回拨问题 |
| 数据库号段 | 预分配号段,如 [1, 1000] | 简单,ID 连续 | 依赖数据库 |
| Redis INCR | Redis 原子递增 | 高性能 | 依赖 Redis |
| Leaf(美团) | 号段 + Snowflake 双模式 | 生产经验丰富 | 需要部署服务 |
Snowflake 雪花算法 ID 结构(64 位)
// | 0 | 41位时间戳(毫秒) | 10位机器ID | 12位序列号 |
// | 1 | 41 bits | 10 bits | 12 bits |
//
// 41位时间戳:可用约 69 年
// 10位机器ID:最多 1024 个节点
// 12位序列号:每毫秒每节点 4096 个 ID
2. 跨分片查询
分片键查询可以路由到具体分片,但非分片键查询需要广播到所有分片:
-- ✅ 命中分片(假设按 user_id 分片)
SELECT * FROM orders WHERE user_id = 1001;
-- ❌ 需要广播所有分片(无分片键)
SELECT * FROM orders WHERE order_no = 'ORD202401001';
-- ❌ 需要归并排序
SELECT * FROM orders ORDER BY create_time DESC LIMIT 10;
解决方案:
- 冗余字段:常用查询条件冗余到分片键中
- 全局表:不分片,所有库中保留完整副本(如字典表)
- 绑定表:有关联关系的表使用相同的分片策略,保证 JOIN 在同一分片
- 二级索引表:建立
order_no → user_id的映射表
3. 跨分片事务
分库后无法使用单库事务,需要分布式事务:
| 方案 | 说明 | 适用场景 |
|---|---|---|
| XA 分布式事务 | 两阶段提交,强一致 | 对一致性要求极高 |
| 柔性事务(TCC) | Try-Confirm-Cancel | 业务可拆分 |
| 最终一致性(Saga) | 补偿机制 | 长事务 |
| 消息队列 | 本地事务 + 消息确保最终一致 | 异步场景 |
4. 跨分片排序和分页
查询 ORDER BY create_time LIMIT 10,需要从每个分片都查出 Top 10,再在内存中归并排序取最终 Top 10。
分页更复杂:LIMIT 100, 10 需要从每个分片查出前 110 条,归并后取第 101-110 条。
5. 扩容问题
取模分片扩容需要数据迁移。解决方案:
- 提前规划:一次分够(如分 1024 张表),通过逻辑表到物理表的映射控制实际表数
- 一致性哈希:扩容只迁移少量数据
- 翻倍扩容:从 N 变 2N,只需迁移一半数据
常用中间件
| 中间件 | 类型 | 说明 |
|---|---|---|
| ShardingSphere-JDBC | 客户端分片 | Java 端 jar 包,无需代理,性能好 |
| ShardingSphere-Proxy | 代理层分片 | 独立进程,对应用透明 |
| MyCat | 代理层分片 | 社区活跃度下降 |
| Vitess | 代理层分片 | YouTube 开源,云原生 |
ShardingSphere-JDBC 分片配置示例
spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
url: jdbc:mysql://localhost:3306/db0
ds1:
url: jdbc:mysql://localhost:3306/db1
rules:
sharding:
tables:
orders:
actual-data-nodes: ds$->{0..1}.orders_$->{0..3}
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: orders-inline
key-generate-strategy:
column: id
key-generator-name: snowflake
sharding-algorithms:
orders-inline:
type: INLINE
props:
algorithm-expression: orders_$->{user_id % 4}
key-generators:
snowflake:
type: SNOWFLAKE
常见面试问题
Q1: 什么时候需要分库分表?
答案:
一般来说,满足以下条件时考虑分库分表:
- 单表数据量超过 2000 万行或 单表数据文件超过 20GB,查询性能明显下降
- 数据库 QPS 超过单机上限(CPU、连接数、磁盘 I/O 瓶颈)
- 已经做了 SQL 优化、索引优化、读写分离、缓存,仍无法满足需求
需要注意:分库分表是一个高成本操作,会引入分布式事务、跨分片查询、ID 生成等复杂问题,不应作为首选方案。
Q2: 垂直拆分和水平拆分的区别?
答案:
| 维度 | 垂直拆分 | 水平拆分 |
|---|---|---|
| 拆分方式 | 按业务/字段拆分 | 按数据行拆分 |
| 解决问题 | 表字段过多、业务解耦 | 单表数据量过大 |
| 拆分后 | 每个库/表的结构不同 | 每个库/表的结构相同 |
| 复杂度 | 相对较低 | 较高(分布式 ID、跨分片查询等) |
实际项目中通常先垂直后水平:先按业务模块拆库(微服务各自独立数据库),再对数据量大的表做水平分片。
Q3: 分片键如何选择?
答案:
选择分片键的核心原则:
- 高频查询字段:大部分查询都应包含分片键,避免广播查询。例如 C 端业务以
user_id为分片键,B 端以merchant_id为分片键 - 数据均匀分布:分片键的值应分布均匀,避免数据倾斜(某些分片数据远多于其他分片)
- 尽量避免跨分片操作:相关联的数据应在同一分片(如同一用户的订单和订单明细)
常见选择:用户 ID、订单 ID、租户 ID 等业务标识。
Q4: 分布式 ID 生成方案有哪些?
答案:
主流方案对比:
| 方案 | 有序性 | 性能 | 依赖 | 适用场景 |
|---|---|---|---|---|
| Snowflake | 趋势递增 | 极高(本地生成) | 时钟 | 大多数场景首选 |
| 号段模式 | 连续递增 | 高 | 数据库 | 需要连续 ID |
| UUID | 无序 | 高(本地生成) | 无 | 无序要求的场景 |
| Redis INCR | 连续递增 | 高 | Redis | 简单场景 |
Snowflake 是最常用的方案:64 位 long 型,由时间戳(41 位)+ 机器 ID(10 位)+ 序列号(12 位)组成,每毫秒每机器可生成 4096 个 ID。需要注意时钟回拨的处理(可以等待或使用上次时间戳)。
Q5: 分库分表后如何做分页查询?
答案:
这是分库分表最痛苦的问题之一。ORDER BY x LIMIT offset, size 需要:
- 向所有分片发送
LIMIT 0, offset+size的查询 - 在内存中合并所有结果,重新排序
- 取出第 offset+1 到 offset+size 条数据
offset 越大,性能越差。
优化方案:
- 禁止深分页:限制最大页码,产品上引导用户缩小条件
- 游标分页:记住上一页最后一条的排序值,下一页从该位置继续查(但不支持跳页)
- 二次查询法:先查出 offset/N 附近的范围,再精确查询
- 搜索引擎:将需要复杂查询的数据同步到 Elasticsearch