2. 分片键设计核心原则
分片键(Shard Key)是 MongoDB 分片集群中决定数据分布与查询路由的核心字段组合。它一旦选定,后续修改成本极高,因此设计阶段必须从五个维度系统评估:基数(Cardinality)、频率(Frequency)、单调性(Monotonicity)、读写模式匹配(Query & Write Pattern Fit)、不可变性(Immutability)。这五个维度并非孤立指标,而是相互制约的权衡体系。
一、基数:决定可切分的“粒度上限”
基数指分片键可能取值的数量。基数越高,chunk 越容易均匀切分到多个分片;基数过低(如布尔值、性别、状态枚举)会导致大量文档挤在少数取值上,无法进一步分裂。例如以 { status: 1 } 作为分片键,若 90% 文档 status 为 "active",则无论集群有多少分片,这部分数据只能落在同一个 chunk 上,形成不可分裂的热点。
判断标准:分片键的取值数量应远大于分片数量,且分布尽量均匀。经验上,基数至少应达到分片数的数十倍以上。但高基数并不等于好分片键——若某个高频取值占比过大,仍会形成热点,这就引出频率维度。
二、频率:识别“高频取值”带来的倾斜
频率指各取值在文档中出现的比例。理想情况下,每个取值对应的文档数接近。若某取值占比过高,即使基数很高,该取值对应的 chunk 也会成为写入与查询热点。例如以 { tenantId: 1 } 分片,若某大客户占 60% 数据量,则其所在分片必然过载。
修正思路是引入复合分片键,将高频字段与高基数、低相关性的字段组合,例如 { tenantId: 1, orderId: 1 },使同一租户的数据仍能按 orderId 继续切分。
三、单调性:避免“永远写最后一个 chunk”
单调递增或递减的字段(如 ObjectId、时间戳、自增 ID)作为分片键时,所有新写入都会落在最大或最小 chunk 上,形成写入热点。这是最典型的分片键反模式之一。MongoDB 的 hashed 分片策略正是为解决单调性问题而设计:对分片键做哈希后分布,使写入均匀散开,代价是范围查询需要广播到所有分片。
四、读写模式匹配:让查询“定向”而非“广播”
分片键必须能支撑最频繁的查询模式。若查询条件包含分片键,mongos 可直接路由到目标分片(targeted query);否则需广播到所有分片(scatter-gather),延迟与资源消耗显著上升。设计时应统计高频查询的过滤字段,优先选择能覆盖这些字段的分片键,或使用复合分片键的前缀匹配。
五、不可变性:写入后不可更改的硬约束
MongoDB 要求分片键字段值在文档写入后不可修改(除非使用 4.4+ 的 refineCollectionShardKey 调整键结构,但字段值本身仍不可变)。因此分片键应选择业务上天然稳定的字段,避免使用会随业务变化的属性(如用户等级、订单状态)。
六、常见反模式与修正方法
反模式一:低基数键。如 { status: 1 }、{ gender: 1 }。修正:改用高基数复合键,或使用 hashed 策略。
反模式二:单调递增键。如 { _id: 1 }、{ createdAt: 1 }。修正:使用 hashed 分片键,或将单调字段与高基数随机字段组合,如 { userId: 1, createdAt: 1 }。
反模式三:高频取值键。如 { tenantId: 1 } 且某租户占绝对多数。修正:复合分片键 { tenantId: 1, orderId: 1 },使同一租户数据继续切分。
反模式四:与查询模式不匹配。如按 { userId: 1 } 分片,但高频查询按 { orderId: 1 }。修正:重新评估查询模式,选择覆盖高频查询的字段,或建立复合索引配合分片键前缀。
七、设计检查清单
| 维度 | 检查问题 | 合格标准 |
|---|---|---|
| 基数 | 取值数量是否足够多? | ≥ 分片数 × 50 |
| 频率 | 是否存在占比 > 30% 的取值? | 各取值文档数接近 |
| 单调性 | 是否随时间递增/递减? | 非单调或使用 hashed |
| 读写匹配 | 高频查询是否包含分片键? | 可定向路由,避免广播 |
| 不可变性 | 字段值写入后是否稳定? | 业务上天然不可变 |
db.collection.aggregate([{ $group: { _id: "$候选字段", count: { $sum: 1 } } }, { $sort: { count: -1 } }]) 统计候选字段的取值分布,验证基数与频率是否满足要求。对于写入密集型场景,优先考虑 hashed 分片键;对于范围查询密集型场景,优先考虑复合分片键并确保查询前缀匹配。
refineCollectionShardKey 添加后缀字段,但无法更改已有字段值,也无法降低基数或消除已有热点。设计阶段的验证成本远低于后期迁移成本。