分表字段如何选择?

#中间件#面试八股

分表字段如何选择?

结论:分片键(Sharding Key)要同时满足四条:基数足够高、分布足够匀、绝大多数查询都带它、写事务尽量落在单片。订单域常见做法是「按 user_id 分片,再把用户 ID 的基因(Gene)嵌进 order_id」,让订单号也能直接路由,避免按单号查询时全表扇出。

一、候选字段评估

候选字段基数均匀性查询覆盖可变性结论
user_id高(亿级)好C 端查询基本全覆盖不可变首选
order_id极高好只覆盖按单号查不可变需基因法辅助
merchant_id中(长尾)差,大商户倾斜B 端查询不可变易倾斜,慎用
create_time高新数据集中报表、归档不可变适合分区或归档
status极低极差很少单独查可变不可用
phone、id_card高好部分查询可能变更更新即迁移,慎用

二、选择原则

  1. 高基数:取值数量要远大于分片数量,否则必然多值同桌。
  2. 分布均匀:业务上不能存在超级大客户,否则一拆就倾斜。
  3. 查询必带:八成以上的核心查询都携带该字段,否则就是跨分片扇出。
  4. 不可变:分片键一改就要跨库搬数据。
  5. 事务内聚:同一笔业务的相关表用同一个键,便于本地事务。
 查询是否总带该字段? ── 否 ──► 不适合做主分片键(做二级索引或基因法)
        │ 是
 基数是否远大于分片数? ── 否 ──► 必然多值同桌,换高基数字段
        │ 是
 分布是否长尾? ── 是 ──► 需拆分热点或独立库
        │ 否
 是否会被更新? ── 是 ──► 迁移成本高,尽量换成不可变键
        │ 否
     可以作分片键

三、基因法:一个键满足两个查询维度

把 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 也能路由。
  • 长尾业务键(商户、店铺)要么拆分热点,要么单独建索引表。
笔记加载中…