分表字段如何选择?
结论:分片键(Sharding Key)要同时满足四条:基数足够高、分布足够匀、绝大多数查询都带它、写事务尽量落在单片。订单域常见做法是「按
user_id分片,再把用户 ID 的基因(Gene)嵌进order_id」,让订单号也能直接路由,避免按单号查询时全表扇出。
一、候选字段评估
| 候选字段 | 基数 | 均匀性 | 查询覆盖 | 可变性 | 结论 |
|---|---|---|---|---|---|
user_id | 高(亿级) | 好 | C 端查询基本全覆盖 | 不可变 | 首选 |
order_id | 极高 | 好 | 只覆盖按单号查 | 不可变 | 需基因法辅助 |
merchant_id | 中(长尾) | 差,大商户倾斜 | B 端查询 | 不可变 | 易倾斜,慎用 |
create_time | 高 | 新数据集中 | 报表、归档 | 不可变 | 适合分区或归档 |
status | 极低 | 极差 | 很少单独查 | 可变 | 不可用 |
phone、id_card | 高 | 好 | 部分查询 | 可能变更 | 更新即迁移,慎用 |
二、选择原则
- 高基数:取值数量要远大于分片数量,否则必然多值同桌。
- 分布均匀:业务上不能存在超级大客户,否则一拆就倾斜。
- 查询必带:八成以上的核心查询都携带该字段,否则就是跨分片扇出。
- 不可变:分片键一改就要跨库搬数据。
- 事务内聚:同一笔业务的相关表用同一个键,便于本地事务。
查询是否总带该字段? ── 否 ──► 不适合做主分片键(做二级索引或基因法)
│ 是
基数是否远大于分片数? ── 否 ──► 必然多值同桌,换高基数字段
│ 是
分布是否长尾? ── 是 ──► 需拆分热点或独立库
│ 否
是否会被更新? ── 是 ──► 迁移成本高,尽量换成不可变键
│ 否
可以作分片键
三、基因法:一个键满足两个查询维度
把 user_id 的低位比特写进 order_id,生成订单号时就保留了路由信息:
private static final int SHARD_BITS = 4; // 16 张表
private static final int SHARD_MASK = (1 << SHARD_BITS) - 1;
private static final long EPOCH = 1_700_000_000_000L;
// 生成订单号:高位时间 + 中位序列 + 最低位 user 基因
long newOrderId(long userId, long seq) {
long gene = userId & SHARD_MASK;
long ts = System.currentTimeMillis() - EPOCH;
return (ts << 23) | ((seq & 0x7FFFF) << SHARD_BITS) | gene;
}
// 路由:无论拿 user_id 还是 order_id,算出的分片都一致
int shardOfUserId(long userId) { return (int) (userId & SHARD_MASK); }
int shardOfOrderId(long orderId) { return (int) (orderId & SHARD_MASK); }
前提:分片数为 2 的幂,且基因必须放在最低位——序列号不能覆盖它,否则路由信息丢失。
四、常见追问
Q1:订单按 user_id 分片,商家后台按 merchant_id 查怎么办?
三条路:建 merchant_id → user_id 的映射表;把订单同步一份到搜索集群按商家维度检索;用双写表在商家维度再存一份(存储翻倍,但查询最直接)。
Q2:分片键以后能改吗? 代价极大,等于全量重分布。必须改时走「新表双写 + 存量迁移 + 灰度切读」,参考迁移设计那一节。
Q3:为什么不用 UUID 当分片键? UUID 无业务含义且完全随机,路由虽然均匀,但无法支持任何维度或范围查询,也无法做基因法,还会让 InnoDB 主键插入随机化、引发页分裂。
Q4:分片键可以用手机号吗? 可以,但手机号会换绑。一旦变更就要跨分片搬数据,通常改为「用户 ID 分片 + 手机号建二级索引表」。
Q5:分片键一定要是主键吗? 不必。分片键是路由依据,主键只需全局唯一;两者可以不同,但用基因法把分片信息写进主键,能省掉一次映射查询。
反例是把 create_time 当分片键:新数据永远落在最后一片,写热点无解,查历史又必须带时间范围。
小结
- 分片键四要素:高基数、均匀、查询必带、不可变。
- 单维度覆盖不全时,用基因法让第二个 ID 也能路由。
- 长尾业务键(商户、店铺)要么拆分热点,要么单独建索引表。