分库分表的数量为什么一般选择 2 的幂?
结论:分片数取 2 的幂,核心是让取模(Modulo)退化为位与(Bitwise AND):
hash & (n-1)与hash % n在 n 为 2 的幂时结果一致,省掉一次除法;更关键的是 2 倍扩容时只需看新增的那一个比特,就能判断某个键要不要搬走,把扩容代价从全量重分布降到 50%。
一、位运算与取模等价
| n | 二进制 | hash % n | hash & (n-1) | 是否恒等 |
|---|---|---|---|---|
| 4 | 100 | 3 | 3 | 是 |
| 8 | 1000 | 5 | 5 | 是 |
| 10 | 1010 | 5 | 5(个别值巧合相等) | 否 |
| 16 | 10000 | 7 | 7 | 是 |
| 1024 | 1 0000 0000 | 1023 | 1023 | 是 |
等价的前提是 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 的幂更大的意义是保留弹性,而不是省那点除法开销。