分库分表
问题
什么时候需要分库分表?垂直拆分和水平拆分有什么区别?常见的分片策略有哪些?分布式 ID 如何生成?
答案
一、为什么需要分库分表?
| 瓶颈 | 表现 | 解决方案 |
|---|---|---|
| 单表数据量过大 | 查询变慢,索引效率下降 | 水平分表 |
| 单库并发过高 | 连接数不够,QPS 达上限 | 分库 |
| 单库存储过大 | 磁盘空间不足 | 分库 |
| 单表字段过多 | 大字段影响热数据查询 | 垂直分表 |
分库分表的时机
- 单表数据量超过 2000 万行或单表大小超过 10GB 时考虑(经验值,非绝对)
- 能用读写分离、缓存、索引优化解决的,不要急于分库分表
- 分库分表一旦实施,会带来巨大的架构复杂性
二、拆分策略
垂直拆分
垂直分表:将一张「宽表」的列拆分为多张表
原则:
- 热数据和冷数据分离
- 大字段(TEXT/BLOB)独立分表
- 按业务模块拆分
垂直分库:按业务域拆分到不同数据库
水平拆分
水平分表:将一张表的数据(行)按某种规则分散到多张结构相同的表
水平分库:在分表的基础上,将表分散到不同的数据库实例
三、分片策略详解
1. 取模分片(Hash)
分片编号 = sharding_key % 分片数量
| 优点 | 缺点 |
|---|---|
| 数据均匀分布 | 扩容困难,需要数据迁移 |
| 实现简单 | 范围查询需要跨所有分片 |
2. 范围分片(Range)
分片 0: id 1 ~ 10000000
分片 1: id 10000001 ~ 20000000
分片 2: id 20000001 ~ 30000000
| 优点 | 缺点 |
|---|---|
| 范围查询效率高 | 可能出现热点(新数据都写最后一个分片) |
| 扩容方便,只需添加新分片 | 数据分布不均匀 |
3. 一致性哈希
通过哈希环解决取模分片扩容困难的问题。扩容时只需要迁移部分数据。
4. 按时间分片
适合日志、订单等有明显时间特征的数据:
orders_202401, orders_202402, orders_202403 ...
选择分片键的原则
| 原则 | 说明 |
|---|---|
| 高频查询条件 | 分片键应是大部分查询的 WHERE 条件 |
| 数据均匀 | 避免某些分片数据过多 |
| 避免跨分片查询 | 尽量让同一业务的查询落在同一分片 |
四、分布式 ID 方案
分库分表后,自增 ID 不再全局唯一,需要分布式 ID 方案:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| UUID | 128 位随机 | 简单,无依赖 | 无序,索引效率低,占空间 |
| 雪花算法 | 时间戳 + 机器ID + 序列号 | 有序,高性能 | 依赖时钟,时钟回拨问题 |
| 号段模式 | 从 DB 批量获取 ID 段 | 高可用,性能好 | 依赖数据库 |
| Redis INCR | 原子自增 | 简单,高性能 | 依赖 Redis 可用性 |
雪花算法(Snowflake)
0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000
| 41 位时间戳 | 5位DC | 5位机器| 12 位序列号
- 41 位时间戳:可用约 69 年
- 10 位机器标识:最多 1024 个节点
- 12 位序列号:每毫秒最多 4096 个 ID
号段模式(如 Leaf-Segment)
-- ID 分配表
CREATE TABLE leaf_alloc (
biz_tag VARCHAR(128) NOT NULL, -- 业务标识
max_id BIGINT NOT NULL DEFAULT 1, -- 当前最大 ID
step INT NOT NULL, -- 每次获取的 ID 数量
PRIMARY KEY (biz_tag)
);
每次从数据库获取一个号段(如 1~1000),在内存中分配,用完再取下一批。
五、分库分表带来的问题
| 问题 | 说明 | 解决方案 |
|---|---|---|
| 跨分片查询 | JOIN、聚合函数需要跨多个分片 | 冗余字段,应用层聚合 |
| 跨分片事务 | 不支持本地事务 | 分布式事务(XA/TCC/Saga) |
| 全局排序 | ORDER BY 需要归并排序 | 中间件层归并 |
| 全局分页 | 深度分页性能差 | 禁止深度分页,用游标分页 |
| ID 全局唯一 | 自增 ID 不可用 | 分布式 ID 方案 |
| 非分片键查询 | 没有分片键的查询需要广播 | 建立映射表或使用 ES |
| 扩容困难 | 增加分片需要数据迁移 | 提前规划,一致性哈希 |
六、分库分表中间件
| 中间件 | 类型 | 特点 |
|---|---|---|
| ShardingSphere-JDBC | 客户端代理(JAR) | 轻量,无额外部署 |
| ShardingSphere-Proxy | 服务端代理 | 语言无关,对应用透明 |
| MyCat | 服务端代理 | 功能丰富,社区活跃度下降 |
| Vitess | 服务端代理 | YouTube 出品,支持 Kubernetes |
常见面试问题
Q1: 分库分表后,不带分片键的查询怎么处理?
答案:
比如订单表按 user_id 分片,但需要按 order_id 查询:
- 映射表:维护
order_id → user_id的映射关系 - 冗余一份:额外按
order_id维护一份索引表 - 搜索引擎:将数据同步到 Elasticsearch,走 ES 查询
- 基因法:将分片信息嵌入 ID 中(如 order_id 末几位包含 user_id 信息)
Q2: 如何实现不停机的分库分表迁移?
答案:
双写方案:
- 新增分片表,应用层同时写老表和新表
- 历史数据通过后台任务逐步迁移到新表
- 数据校验,确保老表和新表数据一致
- 切换读流量到新表
- 停止双写,废弃老表
关键点:需要有数据校验机制,保证迁移的正确性。
Q3: 分库分表和分区表有什么区别?
答案:
| 对比项 | 分库分表 | 分区表 |
|---|---|---|
| 实现层 | 应用层/中间件 | MySQL 内部 |
| 跨机器 | 支持 | 不支持(单机) |
| 运维 | 复杂 | 简单 |
| 连接数 | 分散到多个实例 | 单实例 |
| 适用场景 | 数据量极大,需要分散到多机器 | 单机内的数据管理优化 |
Q4: 2000 万行是怎么来的?
答案:
这是一个经验值。InnoDB B+ 树默认 3 层,每页 16KB:
- 非叶子节点可存约 1170 个指针(假设键 8B + 指针 6B)
- 叶子节点可存约 16 行(假设每行 1KB)
- 3 层最多:1170 × 1170 × 16 ≈ 2190 万行
超过这个量,B+ 树可能变 4 层,多一次磁盘 IO,查询性能下降。但实际阈值取决于行大小。