分库分表的取模算法策略,怎么避免数据倾斜?
结论:取模(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 的幂、先哈希再取模、预分片,是三个成本最低的保险。
- 扩容优先选双倍扩容,只迁移一半,回滚也简单。