复合分片键是当前对抗数据倾斜最可控、最可验证的手段之一,需满足高基数、低相关性、写入时间解耦三点;诊断倾斜需执行distinct和aggregate两步检查;避免单调递增键陷阱及低基数字段组合。

复合分片键不是万能解药,但它是目前对抗数据倾斜最可控、最可验证的手段之一——前提是字段组合真正具备高基数+低相关性+写入时间解耦这三点。
怎么判断当前分片键是否已倾斜?
别等报警才查。直接连上 mongos 执行两步诊断:
-
db.collection.distinct("shard_key_field").length—— 若结果远小于 1000(百万级集合),说明基数严重不足 -
db.collection.aggregate([{$group: {_id: "$shard_key_field", count: {$sum: 1}}}, {$sort: {count: -1}}, {$limit: 10}])—— 若前 10 项文档数之和占比超 30%,就是典型倾斜
注意:_id 作为默认分片键时,若业务用的是自增整数或毫秒时间戳,天然会把新写入集中到最新 chunk,这就是“单调递增键陷阱”,sh.status() 里能看到某个分片 chunk 数极少但大小爆炸。
复合分片键为什么比单字段更可靠?
单字段容易踩中“高活跃度=高集中度”的坑,比如 user_id;而复合键通过引入第二个维度(通常是时间类字段),把原本线性增长的写入打散成二维网格。
- 推荐组合:{
user_id: 1,order_date: 1} 或 {tenant_id: 1,_id: "hashed"} —— 前者保查询局部性,后者防写入热点 - 必须避免组合:{
status: 1,created_at: 1} ——status只有 few 值(如 "pending"/"done"),基数太低,第二字段根本起不了打散作用 - hash 分片不能嵌套:MongoDB 不支持 {
user_id: "hashed",order_date: 1},要么全 range,要么首字段 hash 后其余字段必须是 range
上线前必须做的三件事
复合键改完不等于问题消失,很多团队卡在迁移后才发现查询变慢或路由失效:
- 确认所有高频查询都包含复合键前缀字段,否则
mongos无法精准路由,退化为广播查询 - 检查索引:原单字段索引(如
{user_id: 1})对复合键无效,必须重建为{user_id: 1, order_date: 1}或至少{user_id: 1, order_date: 1, ...} - 测试 chunk 拆分行为:插入一批模拟数据后,运行
sh.status()观察 chunk 是否开始均匀落在各分片;若仍集中在 1–2 个分片,说明组合字段实际分布仍不均(比如order_date全是同一天)
最容易被忽略的一点:复合键的“第二字段”必须是业务写入时真实可控、且天然分散的字段。拿 new Date().toISOString().slice(0,10) 当 order_date 看似合理,但如果批量导入订单全用同一日期,照样白搭。真实场景中,得盯住写入逻辑本身是否带有时序偏差。











