唯一业务键必须与分片键联动设计,否则分片集群中unique索引仅单分片生效;根本原因是MongoDB不跨分片校验唯一性;有效方案是将业务唯一字段设为分片键或复合分片键一部分。

唯一业务键不是“加个索引就完事”,而是必须和分片键、写入逻辑、数据清洗联动设计。否则在分片集群里,unique: true 看似生效,实则只在单个分片内起作用。
分片集群中唯一索引为什么失效
根本原因是:MongoDB 的 createIndex({field: 1}, {unique: true}) 只在每个分片上独立建索引,不跨分片校验。如果 url 不是分片键,两条相同 url 的文档可能被路由到不同分片,插入成功且不报错。
- 错误现象:
insertOne不报错,但db.collection.find({url: "x"})返回多条 - upsert 也可能失效——查询条件没带完整分片键时,mongos 无法精准定位目标分片
- 唯一索引在哈希分片(
{field: "hashed"})下同样不保全局唯一,除非该字段本身就是分片键或复合分片键的一部分
真正有效的唯一业务键设计方式
核心原则:让相同业务键值的文档强制落到同一分片。这只能靠分片键设计实现,不是靠索引补救。
- 首选方案:把业务唯一字段设为分片键,例如
sh.shardCollection("db.users", {"email": 1}) - 次选方案:用复合分片键 + 应用层预计算,例如
{"tenant_id": 1, "hash_email": "hashed"},写入前用相同哈希算法算出hash_email值 - 绝对避免:仅靠
email字段建唯一索引却不把它放进分片键——这是生产环境重复数据的高发源头
建索引前必须做的三件事
即使分片键设计合理,建唯一索引仍可能失败或行为异常,必须提前清理和验证。
- 检查已有重复:
db.collection.aggregate([{$group: {_id: "$email", count: {$sum: 1}}}, {$match: {count: {$gt: 1}}}], {allowDiskUse: true}) - 确认字段值一致性:用
.distinct()看实际值分布,排查空格、大小写、null、空字符串等隐藏差异 - 按需配置选项:
- 大小写不敏感?加
collation: {locale: "en", strength: 2} - 允许 null 但需视为唯一占位?改用
sparse: true - 数组字段参与唯一性?确保理解多键索引行为(如
"emails.0"和"emails"效果不同)
- 大小写不敏感?加
应用层写入逻辑不能绕过分片键约束
即便做了上述所有,应用层若忽略分片键,仍会触发路由错误或竞态问题。
- upsert 必须带完整分片键条件,否则报错
Cannot upsert with query that does not contain the shard key - 并发写入场景下,仅靠
find + insert有竞态窗口,必须依赖唯一索引抛出E11000 duplicate key error来重试 - 永远用
$set而非全量文档更新,避免未传字段被清空;必要时组合$setOnInsert
最易被忽略的一点:分片键一旦设定就无法修改(除非导出重建),所以业务唯一字段是否适合作为分片键,必须在建库初期就拍板——而不是等上线后发现重复数据再补救。


















