分库分表规范
分片策略、路由规则
【Standard】单表数据量超过 1000 万行或单库容量超过 500GB 时评估分库分表;优先分表,再考虑分库;分片键选择高频查询且数据分布均匀的字段。
| 方式 | 说明 | 适用场景 |
|---|---|---|
| 垂直分库 | 按业务域拆分到不同数据库 | 业务解耦、独立扩展 |
| 垂直分表 | 大字段拆分到扩展表 | 表字段过多、冷热分离 |
| 水平分库 | 同一表数据按分片键拆分到多个库 | 数据量极大、写压力高 |
| 水平分表 | 同一库内拆分为多个物理表 | 单表数据量大、写压力适中 |
- 【Requirement】分片键必须是高频查询条件,大部分查询能定位到单分片
- 【Standard】分片键取值分布均匀,避免数据倾斜(某分片数据量是其他的 3 倍以上)
- 【Standard】分片键值不可变,避免数据迁移
- 【Caution】禁止使用自增 ID 作为分片键,无法均匀分布
| 策略 | 算法 | 说明 |
|---|---|---|
| 取模 | hash(key) % N | 均匀分布,扩容需迁移 |
| 范围 | 按时间/ID 区间 | 便于范围查询,易倾斜 |
| 一致性哈希 | 哈希环 | 扩容迁移少,实现复杂 |
| 查表法 | 分片键与分片映射表 | 灵活,需维护映射 |
- 分表数量建议为 2 的幂(32、64、128),便于后续扩容
- 单表目标数据量控制在 500 万 - 1000 万
- 提前规划 2-3 年的数据量,避免频繁扩容
示例:按 user_id 取模分 64 表
table_name = order_order_ + (user_id % 64)【Requirement】所有分片表的查询必须带分片键;不带分片键的查询必须走广播(全分片)且需审批。
| 查询类型 | 路由方式 | 说明 |
|---|---|---|
| 带分片键等值查询 | 单分片路由 | 直接定位到具体分片 |
| 带分片键范围查询 | 多片路由 | 扫描分片键涉及的分片 |
| 不带分片键 | 全分片广播 | 必须评估性能,加缓存 |
| 非分片键查询 | 全分片广播 | 建议走 ES 或冗余表 |
-- 正确:带分片键 user_id
SELECT * FROM order_order WHERE user_id = 123 AND order_status = 1;
-- 需审批:不带分片键,全分片广播
SELECT * FROM order_order WHERE order_no = 'ORD20240101001';
-- 禁止:不带分片键的大范围扫描
SELECT * FROM order_order WHERE create_time >= '2024-01-01' LIMIT 1000;【Caution】分片表的唯一索引必须包含分片键,否则无法保证全局唯一。
-- 正确:唯一索引包含分片键
UNIQUE KEY uk_user_order (user_id, order_no)
-- 错误:全局唯一索引在分片表无法保证
UNIQUE KEY uk_order_no (order_no) -- 只能保证分片内唯一如需全局唯一,使用发号器生成唯一业务 ID(如雪花 ID)。
【Requirement】禁止在分片表上执行跨分片 JOIN,必须在应用层组装。
// 正确:应用层组装
List<Order> orders = orderMapper.selectByUserId(userId); // 单片查询
List<Long> productIds = orders.stream().map(Order::getProductId).collect(toList());
List<Product> products = productMapper.selectByIds(productIds); // 商品表分片键不同,单独查
Map<Long, Product> productMap = products.stream().collect(toMap(Product::getId, p -> p));
orders.forEach(o -> o.setProductName(productMap.get(o.getProductId()).getName()));
// 错误:跨分片 JOIN
// SELECT o.*, p.name FROM order_order o JOIN product_product p ON o.product_id = p.id WHERE o.user_id = ?【Requirement】分片表主键必须全局唯一,使用雪花算法或号段模式,禁止使用数据库自增 ID。
| 方案 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| 雪花算法 (Snowflake) | 高性能、趋势递增 | 时钟回拨问题 | ⭐⭐⭐⭐⭐ |
| 号段模式 (Segment) | 可控、无时钟问题 | 依赖 DB、有号段空洞 | ⭐⭐⭐⭐ |
| UUID | 简单 | 无序、索引性能差 | ⭐⭐ |
| Redis INCR | 简单 | 依赖 Redis、单点 | ⭐⭐ |
0 | 41位时间戳 | 10位机器ID | 12位序列号- 41 位时间戳:可用约 69 年
- 10 位机器 ID:支持 1024 个节点
- 12 位序列号:每节点每毫秒 4096 个 ID
// 雪花算法 ID 生成
long id = snowflake.nextId();-- 号段表
CREATE TABLE sys_id_segment (
biz_tag VARCHAR(64) NOT NULL COMMENT '业务标识',
max_id BIGINT NOT NULL DEFAULT 0 COMMENT '当前最大ID',
step INT NOT NULL DEFAULT 1000 COMMENT '步长',
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (biz_tag)
) COMMENT='号段表';应用批量获取号段,内存中分配,耗尽后再取下一批。
【Caution】跨片查询应尽量避免;必须跨片时,使用 ES 做二级索引或异步汇总表,禁止实时全分片扫描。
| 方案 | 适用场景 | 实时性 |
|---|---|---|
| 分片键定位 | 查询带分片键 | 实时 |
| ES 二级索引 | 多条件查询、模糊搜索 | 准实时 |
| 汇总表/宽表 | 报表、统计 | T+1 或准实时 |
| 全分片扫描 | 后台任务、低频 | 慢,需限流 |
将分片表的查询字段同步到 ES,通过 ES 定位分片键,再回源查询:
1. 写入分片表时,同步写 ES(双写或订阅 binlog)2. 查询时先查 ES 获取满足条件的主键列表3. 按分片键分组,到各分片查询详情- 仅用于后台低频任务,禁止在线上接口使用
- 必须限速,每片查询间隔 > 100ms
- 必须分批,每片每次 ≤ 500 条
- 必须带时间范围,避免全表扫描
// 全分片扫描示例(后台任务)
for (int i = 0; i < 64; i++) {
Long lastId = 0L;
while (true) {
List<Order> orders = orderMapper.scanByShard(i, lastId, startTime, endTime, 500);
if (orders.isEmpty()) break;
process(orders);
lastId = orders.get(orders.size() - 1).getId();
Thread.sleep(100); // 限速
}
}跨分片事务禁止使用 2PC,采用最终一致性:
- 本地消息表
- TCC
- Saga
【Standard】分库分表迁移采用「双写 + 灰度切读」方案,保证零停机迁移。
阶段一:准备 1. 搭建分片集群,建分表 2. 开发双写代码(同时写旧表和新分片表) 3. 开发数据校验工具
阶段二:双写 1. 上线双写,新旧表同时写入 2. 历史数据全量迁移(按主键范围分批) 3. 增量数据通过双写同步
阶段三:校验 1. 比对新旧表数据一致性 2. 修复不一致数据 3. 持续校验 3 天无差异
阶段四:灰度切读 1. 1% 流量读新分片表 2. 10% → 50% → 100% 逐步放量 3. 观察指标,无异常则全量切换
阶段五:下线 1. 停止双写,只写新表 2. 旧表保留 1 个月后归档删除| 工具 | 说明 |
|---|---|
| 自研迁移脚本 | 按主键范围分批 select + insert |
| Canal | 订阅 binlog 增量同步 |
| DataX | 阿里云数据同步工具 |
| ShardingSphere-Scaling | ShardingSphere 生态迁移工具 |
- 【Requirement】迁移前必须全量备份
- 【Caution】迁移期间禁止 DDL 变更
- 【Standard】迁移速度限制,避免对源库造成压力(QPS ≤ 1000)
- 【Standard】设置回滚预案,切读阶段出问题可秒级回退到旧表