分库分表的数量为什么一般选择 2 的幂?

#中间件#面试八股

分库分表的数量为什么一般选择 2 的幂?

结论:分片数取 2 的幂,核心是让取模(Modulo)退化为位与(Bitwise AND):hash & (n-1) 与 hash % n 在 n 为 2 的幂时结果一致,省掉一次除法;更关键的是 2 倍扩容时只需看新增的那一个比特,就能判断某个键要不要搬走,把扩容代价从全量重分布降到 50%。

一、位运算与取模等价

n二进制hash % nhash & (n-1)是否恒等
410033是
8100055是
10101055(个别值巧合相等)否
161000077是
10241 0000 000010231023是

等价的前提是 n 为 2 的幂且哈希值为非负;Java 里可用 Math.floorMod(hash, n),或先 hash & 0x7fffffff 再取位。

二、扩容时只迁移一半

n = 4(掩码 011) ──► n = 8(掩码 111)

   原 hash & 011 = 1  ──►  新分片 1   (新增位 bit2 = 0,原地不动)
   原 hash & 011 = 1  ──►  新分片 5   (新增位 bit2 = 1,需要迁移)
                              ▲
                              └── 只看新增的这一位,即可判定去留

对比普通取模扩容(4 → 5):键的目标分片几乎全变,等同全量重写;而 2 倍扩容只有一半的键会换位置,迁移窗口和回滚风险都减半。

三、代码与验证

public final class ShardMath {

    static int index(long hash, int shards) {
        if (Integer.bitCount(shards) != 1) {
            return (int) Math.floorMod(hash, shards);   // 非 2 的幂,退回取模
        }
        return (int) (hash & (shards - 1));
    }

    // 2 倍扩容时是否需要迁移
    static boolean needMove(long hash, int oldShards) {
        return (hash & oldShards) != 0;
    }

    public static void main(String[] args) {
        int oldN = 16, newN = 32, total = 1_000_000, moved = 0;
        for (int i = 0; i < total; i++) {
            long h = mix(i);
            if (needMove(h, oldN)) {
                moved++;
            } else if (index(h, oldN) != index(h, newN)) {
                throw new AssertionError("未标记迁移却换了分片: " + i);
            }
        }
        System.out.printf("迁移比例 %.2f%%%n", moved * 100.0 / total);  // 约 50%
    }

    // 高低位混合:自增 ID 低位相关性很强,必须先扰动
    static long mix(long x) {
        x ^= x >>> 33; x *= 0xff51afd7ed558ccdL;
        x ^= x >>> 33; x *= 0xc4ceb9fe1a85ec53L;
        return x ^ (x >>> 33);
    }
}

四、配置上的对应

sharding:
  tables:
    t_order:
      actual-data-nodes: ds$->{0..7}.t_order_$->{0..15}   # 8 库 × 16 表
      database-strategy:
        standard:
          sharding-column: user_id
          sharding-algorithm-name: db_hash_mod
      table-strategy:
        standard:
          sharding-column: user_id
          sharding-algorithm-name: tbl_hash_mod
      sharding-algorithms:
        db_hash_mod:
          type: HASH_MOD
          props:
            sharding-count: 8
        tbl_hash_mod:
          type: HASH_MOD
          props:
            sharding-count: 16

五、常见追问

Q1:2 的幂一定均匀吗? 不一定。hash & (n-1) 只使用哈希低位,若原始键的低位相关性很强(自增 ID、毫秒时间戳拼接),低位相同的键会挤进同一分片。因此取位之前必须先做一次哈希扰动(如 MurmurHash3),再走位与。

Q2:为什么不是 3、5、10? 除不尽时只能退回除法取模,CPU 开销其实可忽略,但失去了「按位判断迁移」的能力,扩容只能全量重分布。此外业务上需要「每库再对半拆」的弹性,2 的幂天然支持。

Q3:1024 张表会不会太多? 如果每张表都建连接、都单独做 DDL,确实太多。实践中常见「逻辑分片 1024,物理库 4 到 16 个」,把 1024 当作路由空间而非物理表数量。

Q4:扩容时迁移期间新旧路由并存怎么办? 用双写加读兜底:迁移中的键先写旧分片,读到空结果时回查新分片;或用中间件内置的「双倍扩容」迁移流程,按比特逐步切流。

小结

  • 2 的幂让取模变成位与,并让 2 倍扩容只迁移一半数据。
  • 位与只取低位,必须配合哈希扰动,否则均匀性反而不如取模。
  • 定 2 的幂更大的意义是保留弹性,而不是省那点除法开销。
笔记加载中…