跳转到内容

分库分表规范

分片策略、路由规则


【Standard】单表数据量超过 1000 万行或单库容量超过 500GB 时评估分库分表;优先分表,再考虑分库;分片键选择高频查询且数据分布均匀的字段。

方式说明适用场景
垂直分库按业务域拆分到不同数据库业务解耦、独立扩展
垂直分表大字段拆分到扩展表表字段过多、冷热分离
水平分库同一表数据按分片键拆分到多个库数据量极大、写压力高
水平分表同一库内拆分为多个物理表单表数据量大、写压力适中
  1. 【Requirement】分片键必须是高频查询条件,大部分查询能定位到单分片
  2. 【Standard】分片键取值分布均匀,避免数据倾斜(某分片数据量是其他的 3 倍以上)
  3. 【Standard】分片键值不可变,避免数据迁移
  4. 【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-ScalingShardingSphere 生态迁移工具
  • 【Requirement】迁移前必须全量备份
  • 【Caution】迁移期间禁止 DDL 变更
  • 【Standard】迁移速度限制,避免对源库造成压力(QPS ≤ 1000)
  • 【Standard】设置回滚预案,切读阶段出问题可秒级回退到旧表