分库分表的取模算法策略,怎么避免数据倾斜?

#中间件#面试八股

分库分表的取模算法策略,怎么避免数据倾斜?

结论:取模(Modulo)本身是均匀的,倾斜几乎都来自两件事——分片键的分布本身不均匀,取模基数太小。避坑三板斧:选高基数字段做键、对哈希值而非原始值取模、用预分片(逻辑 1024)把扩容动作与取模空间解耦。

一、取模为什么也会倾斜

成因现象对策
分片键基数小于分片数部分分片永远空着增加分片数或更换分片键
键分布长尾大客户集中在一个分片拆分热点、独立库
直接对业务 ID 取模尾号有规律(按天递增)而聚堆先做哈希扰动
取模基数非 2 的幂无法按位迁移,扩容只能全量基数取 2 的幂
大促瞬时写入秒杀订单集中同一片预留分片、队列削峰
订单号按「日期 + 当日序号」生成,直接 % 4:

  2025110300001 % 4 = 1      2025110300002 % 4 = 2
  2025110300003 % 4 = 3      2025110300004 % 4 = 0
  ...
  当日序号连续 ⇒ 四个分片轮流命中,看似均匀;
  但若序号按渠道分段(渠道 A 占 0001~9999),
  渠道 A 的订单会均匀铺满,而「单个渠道的批量导入」
  仍会在短时间内压向同一批分片。

取模解决的是「最终分布」,解决不了「瞬时热点」。

二、预分片:把扩容和取模解耦

逻辑分片 1024 张(定义一次,永不改变)      物理节点(可增减)
 t_order_0000 ┐
 t_order_0001 ├──► ds0   承载 0000~0255
 ...          │
 t_order_0256 ├──► ds1   承载 0256~0511
 ...          │
 t_order_0512 ├──► ds2   承载 0512~0767
 t_order_0768 ├──► ds3   承载 0768~1023

扩容时:把 ds2 上的 256 张表整体搬到 ds4,路由表改一行,分片算法不动

好处是分片算法 hash % 1024 永不变更,扩容退化为「搬表 + 改映射」;代价是逻辑表数量多,需要中间件或自研路由支持。

三、双倍扩容与迁移判定

public class DoubleExpansionRouter {
    private final int oldShards;   // 例如 4
    private final int newShards;   // 8

    public DoubleExpansionRouter(int oldShards) {
        this.oldShards = oldShards;
        this.newShards = oldShards << 1;
    }

    // 只有新增位为 1 的键需要搬迁
    public boolean shouldMigrate(long hash) {
        return (hash & oldShards) != 0;
    }

    // 迁移完成后的路由
    public int route(long hash) {
        return (int) (hash & (newShards - 1));
    }

    // 迁移期间的读兜底:先按旧地址查,迁移中的键回查新地址
    public int readFallback(long hash) {
        int oldIdx = (int) (hash & (oldShards - 1));
        return shouldMigrate(hash) ? oldIdx + oldShards : oldIdx;
    }
}

四、配置示例

sharding-algorithms:
  order_mod:
    type: MOD              # 直接对整数列取模
    props:
      sharding-count: 1024
  tenant_hash_mod:
    type: HASH_MOD         # 先哈希再取模,适用于字符串键
    props:
      sharding-count: 1024

整数 ID 用 MOD 最省;字符串分片键(如 tenant_code)必须用 HASH_MOD,否则字符编码特征会让结果严重倾斜。

五、常见追问

Q1:预分片 1024 会不会太多? 1024 只是路由空间。物理上可以先只建 4 个库、每库 16 张表,用映射表把 1024 个逻辑槽折叠到 64 张物理表上,需要扩容时再展开。

Q2:加盐和取模能一起用吗? 可以,加盐相当于先扩维再取模。但加盐后读要扇出所有盐位,只适合写多读少,不适合列表与范围查询。

Q3:倾斜已经发生了怎么补救? 短期把热点数据迁到独立库或独立表;中期调整分片键(新建索引表);长期在写入侧拆分(给大客户分配独立 ID 空间)。

小结

  • 取模只是函数,倾斜的根因在分片键与基数。
  • 用 2 的幂、先哈希再取模、预分片,是三个成本最低的保险。
  • 扩容优先选双倍扩容,只迁移一半,回滚也简单。
笔记加载中…