分表字段如何选择?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(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+ 小伙伴加入学习,欢迎点击围观
面试考察点
- 基础理解:分片键(Shard Key)是分表的核心,决定数据落到哪张表。面试官想看你是否真的理解它的作用,而不只是会配个 ShardingSphere。
- 业务 sense:选分片键本质上是个业务问题,不是技术问题。面试官想看你能不能结合业务场景分析,而不是背概念。
- 踩坑意识:选错分片键会带来数据倾斜、跨分片查询、热点写等灾难性问题。知道这些坑说明你是真做过。
- 方案落地能力:能不能说出常用分片算法、特殊场景处理(不带分片键怎么查、多维度查询怎么搞)。
核心答案
选分片键,记住三个核心原则:
| 原则 | 含义 | 反面教材 |
|---|---|---|
| 高基数 | 字段值要离散、取值范围广 | 用 status(只有 0/1/2)做分片键 |
| 查询命中 | 大部分核心查询都带这个字段 | 订单表用 merchant_id 分片,但用户端查订单不带它 |
| 稳定不变 | 值确定后不能修改,最好单调递增 | 用 phone_number,用户换号就崩了 |
实战中最常见的分片键:
- C 端用户态业务:
user_id(最经典,没有之一) - 订单业务:
user_id或order_id(看查询入口) - 多租户 SaaS:
tenant_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_id、order_status、create_time 查询,怎么办?
几个方案:
- 异构索引:用 ES 或 HBase 建一份按其他维度的索引,先查索引拿
order_id,再回表 - 双写/多写:同时按
user_id和merchant_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:用雪花算法生成分布式 IDMOD算法:取模分片
六、特殊场景:不带分片键的查询
如果业务上确实没法带分片键,几个应对策略:
- 广播表(Broadcast Table):小表(如字典表、配置表)在每个库都存一份,更新时同步,查询时本地查
- 绑定表(Binding Table):关联表用相同的分片键和分片算法,join 时不用笛卡尔积
- 读写分离:复杂查询走从库,分片表只承载核心链路
面试高频追问
-
追问一:分片键和主键有什么区别?
- 主键是逻辑层面唯一标识一条记录,分片键是物理层面决定数据落到哪。同一个表里,分片键可以是主键,也可以不是。比如订单表,主键是
order_id,分片键可能是user_id。
- 主键是逻辑层面唯一标识一条记录,分片键是物理层面决定数据落到哪。同一个表里,分片键可以是主键,也可以不是。比如订单表,主键是
-
追问二:分表后还能 join 吗?
- 能,但要分情况。同分片键同算法的表(绑定表)join 没问题;不同分片键的表 join 会产生笛卡尔积查询,性能很差,生产基本不可用。
-
追问三:分库分表后 ID 怎么生成?
- 自增 ID 不能用了(会冲突)。常用方案:雪花算法(最主流)、号段模式(Leaf)、UUID(不推荐,太长且无序)。
-
追问四:分表数量怎么定?
- 阿里《Java 开发手册》的建议是单表 500 万行或 2GB 就该考虑分库分表了。业界流传的 “2000 万拐点” 有理论依据:InnoDB 默认 16KB 页大小,B+树 3 层能存约 2190 万行(主键 BIGINT + 行数据 1KB 的假设下),超过这个量 B+树从 3 层变 4 层,磁盘 IO 多一次,性能下降明显。实战中一般预估未来 3-5 年的数据量,一次性分够(如 16、32、64 张表),避免频繁扩容。
常见面试变体
- “你项目里分表分片键是怎么选的?为什么这么选?”
- “订单表如果既要按用户查又要按商家查,分片键怎么搞?”
- “分片键选错了会怎么样?怎么补救?”
- “分库和分表有什么区别?一定要一起做吗?”
记忆口诀
分片键选型口诀:
离散均匀查询带,稳定单调不要变。 取模范围日期分,雪花生成做主键。 多查基因或异构,扩容一致哈希来。
总结
分片键的选择本质上是业务问题,把 离散、查询命中、稳定 这三条卡住,基本不会踩大坑。最经典的组合是 user_id 做分片键 + 雪花算法生成主键 + 取模分片。多维度查询用基因法或异构索引兜底。最后,分片键一旦上线几乎没法改,设计阶段一定要想清楚。
