分表字段如何选择?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 基础理解:分片键(Shard Key)是分表的核心,决定数据落到哪张表。面试官想看你是否真的理解它的作用,而不只是会配个 ShardingSphere。
  2. 业务 sense:选分片键本质上是个业务问题,不是技术问题。面试官想看你能不能结合业务场景分析,而不是背概念。
  3. 踩坑意识:选错分片键会带来数据倾斜、跨分片查询、热点写等灾难性问题。知道这些坑说明你是真做过。
  4. 方案落地能力:能不能说出常用分片算法、特殊场景处理(不带分片键怎么查、多维度查询怎么搞)。

核心答案

选分片键,记住三个核心原则:

原则 含义 反面教材
高基数 字段值要离散、取值范围广 status(只有 0/1/2)做分片键
查询命中 大部分核心查询都带这个字段 订单表用 merchant_id 分片,但用户端查订单不带它
稳定不变 值确定后不能修改,最好单调递增 phone_number,用户换号就崩了

实战中最常见的分片键:

  • C 端用户态业务user_id(最经典,没有之一)
  • 订单业务user_idorder_id(看查询入口)
  • 多租户 SaaStenant_id
  • 日志/流水类create_time(按时间范围分表)

深度解析

一、三个核心原则拆解

分片键选择
分片键选择

分片键选择原则
分片键选择原则

上图是一个分片键选型的快速判断流程,三个条件层层递进,任意一关挂掉都不能选。下面挨个展开说。

高基数:避免数据倾斜

分片键的取值必须足够离散。比如拿 user_id 做分片键,1000 万用户均匀落到 16 张表,每表 62.5 万,没问题。但如果你拿 status(订单状态只有 "待支付/已支付/已发货/已完成" 4 个值)做分片键,4 张表数据完全没法均匀分布。

更隐蔽的坑是 “看起来离散,实际倾斜”。比如用 area_code(地区编码)做分片键,北上广的订单量能占 70%+,照样严重倾斜。

查询命中:避免全分片扫描

这个最关键。分片键选错了,查询不带分片键,ShardingSphere / MyCat 只能 广播 到所有分片,再合并结果。100 张表就查 100 次,性能不升反降。

分片键路由查询
分片键路由查询

举个例子:订单表按 merchant_id 分表,但用户在 App 里查 “我的订单”,查询条件只有 user_id,没带 merchant_id,结果就是全分片扫描。

稳定不变:避免数据迁移

分片键一旦确定,值就不能改。改了就意味着这条数据要从 A 表挪到 B 表,业务代码根本接不住。

更隐蔽的是 “单调递增” 的考量。如果用自增 ID 做分片键 + 取模分片,前几个分片总是先写满(其实是取模后集中在某几个表),可能造成写热点。所以很多大厂用的是 雪花算法(Snowflake),时间戳在前,整体趋势递增,配合取模分片能分散写压力。雪花算法的 64 位结构是:1 位符号位 + 41 位时间戳 + 10 位机器位 + 12 位序列号,同一毫秒单机可生成 4096 个 ID。

二、常用分片算法

算法 说明 优点 缺点
取模分片 hash(user_id) % N 最经典 数据绝对均匀 扩容要迁移大量数据
范围分片 user_id 1-1000 -> 表1 按区间划分 扩容简单 容易写热点(新数据集中在最后一张表)
一致性哈希 节点变化时只迁移部分数据 扩容影响小 实现复杂
日期分片 yyyymm 按月分表 归档方便 历史数据冷热不均
标签分片 按业务字段分(如按 tenant_id 业务隔离清晰 需要预估每个标签数据量

实战中最常用的是 取模分片 + 雪花算法生成 ID。取模保证均匀,雪花保证趋势递增。

ShardingSphere 里对应的算法就叫 MOD(数值型分片键)和 HASH_MOD(字符串等非数值型分片键),还有一个表达式分片算法 INLINE,灵活度更高。

三、选错分片键的三大灾难

灾难 1:数据倾斜

90% 的数据落到一张表,其他表空着。这张表的写入、查询、锁都会成为整个系统的瓶颈。

灾难 2:跨分片查询

业务查询不带分片键,导致每次查询都要广播到所有分片。我有一次接手过一个系统,按 merchant_id 分了 64 张表,结果 80% 的查询都不带这个字段,性能还不如不分。

灾难 3:分布式事务

同一个业务的若干条数据散落在不同分片,更新的时候就要跨分片事务,性能和复杂度都是噩梦。

四、多维度查询怎么办?

这是面试官特别爱追问的点。比如订单表按 user_id 分表,但后台管理要按 merchant_idorder_statuscreate_time 查询,怎么办?

分表异构索引
分表异构索引

几个方案:

  • 异构索引:用 ES 或 HBase 建一份按其他维度的索引,先查索引拿 order_id,再回表
  • 双写/多写:同时按 user_idmerchant_id 存两份(成本高)
  • 广播表/全量表:数据量不大时直接全量冗余
  • 基因法:把 user_id 的几位嵌入到 order_id 里,从 order_id 能反推 user_id,按 order_id 分片也能定位

代码示例:基因法生成 order_id,把 user_id 的后 4 位嵌入 order_id

// user_id 的后 4 bit 作为"基因",对应 16 个分片
long userId = 12345678L;
int userGene = (int) (userId & 0xF); // 取后 4 bit(0~15)

// 用雪花算法生成 order_id,然后把低 4 位替换为基因
long snowflakeId = snowflakeNextId();
long orderId = (snowflakeId & ~0xFL) | userGene;

// 之后无论按 order_id 还是 user_id 取模 16,结果都一样
// 因为 orderId & 0xF == userGene == userId & 0xF

这个方案大众点评的订单系统在用,order_id 的最后 4 位直接就是 user_id 的后 4 位。基因位数决定最大分片数:4 bit 对应 16 库,8 bit 对应 256 库,提前规划好扩容上限。

五、ShardingSphere 实战配置

spring:
  shardingsphere:
    rules:
      sharding:
        tables:
          t_order:
            # 真实数据节点:2 个库,每个库 4 张表
            actual-data-nodes: ds_${0..1}.t_order_${0..3}
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: db-mod
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: tbl-mod
            key-generate-strategy:
              column: order_id
              key-generator-name: snowflake
        sharding-algorithms:
          db-mod:
            type: MOD
            props:
              sharding-count: 2
          tbl-mod:
            type: MOD
            props:
              sharding-count: 4
        key-generators:
          snowflake:
            type: SNOWFLAKE
            props:
              worker-id: 1

注意几个关键点:

  • sharding-column: user_id:明确指定分片键
  • key-generator-name: snowflake:用雪花算法生成分布式 ID
  • MOD 算法:取模分片

六、特殊场景:不带分片键的查询

如果业务上确实没法带分片键,几个应对策略:

  1. 广播表(Broadcast Table):小表(如字典表、配置表)在每个库都存一份,更新时同步,查询时本地查
  2. 绑定表(Binding Table):关联表用相同的分片键和分片算法,join 时不用笛卡尔积
  3. 读写分离:复杂查询走从库,分片表只承载核心链路

面试高频追问

  1. 追问一:分片键和主键有什么区别?

    • 主键是逻辑层面唯一标识一条记录,分片键是物理层面决定数据落到哪。同一个表里,分片键可以是主键,也可以不是。比如订单表,主键是 order_id,分片键可能是 user_id
  2. 追问二:分表后还能 join 吗?

    • 能,但要分情况。同分片键同算法的表(绑定表)join 没问题;不同分片键的表 join 会产生笛卡尔积查询,性能很差,生产基本不可用。
  3. 追问三:分库分表后 ID 怎么生成?

    • 自增 ID 不能用了(会冲突)。常用方案:雪花算法(最主流)、号段模式(Leaf)、UUID(不推荐,太长且无序)。
  4. 追问四:分表数量怎么定?

    • 阿里《Java 开发手册》的建议是单表 500 万行或 2GB 就该考虑分库分表了。业界流传的 “2000 万拐点” 有理论依据:InnoDB 默认 16KB 页大小,B+树 3 层能存约 2190 万行(主键 BIGINT + 行数据 1KB 的假设下),超过这个量 B+树从 3 层变 4 层,磁盘 IO 多一次,性能下降明显。实战中一般预估未来 3-5 年的数据量,一次性分够(如 16、32、64 张表),避免频繁扩容。

常见面试变体

  • “你项目里分表分片键是怎么选的?为什么这么选?”
  • “订单表如果既要按用户查又要按商家查,分片键怎么搞?”
  • “分片键选错了会怎么样?怎么补救?”
  • “分库和分表有什么区别?一定要一起做吗?”

记忆口诀

分片键选型口诀

离散均匀查询带,稳定单调不要变。 取模范围日期分,雪花生成做主键。 多查基因或异构,扩容一致哈希来。

总结

分片键的选择本质上是业务问题,把 离散、查询命中、稳定 这三条卡住,基本不会踩大坑。最经典的组合是 user_id 做分片键 + 雪花算法生成主键 + 取模分片。多维度查询用基因法或异构索引兜底。最后,分片键一旦上线几乎没法改,设计阶段一定要想清楚。