在 mongodb 分片集群中,非片键前缀的唯一索引无法保证全局唯一,仅本地生效;必须以片键为前缀(如片键{orgid:1},则{orgid:1,email:1}可建唯一索引)才能跨分片校验,且应用层需兜底校验。

在 MongoDB 分片集群中,直接对非片键字段创建唯一索引会失败或“看似生效实则无效”——这不是 bug,而是分片架构的硬性约束。唯一索引必须能被路由到单个分片执行校验,否则跨分片去重无法原子保证。
为什么 createIndex({email: 1}, {unique: true}) 在分片集群里不报错但不起作用
这个命令可能成功返回,但插入重复 email 时不会报错。根本原因是:MongoDB 允许你创建该索引,但它只在各分片本地生效,无法跨分片强制唯一。查询路由时,email 不是片键,请求可能发往任意分片,而该分片只检查自己数据,漏掉其他分片的重复值。
- 分片集群中,唯一索引只有在 索引键以片键(shard key)为前缀 时,才能保证全局唯一
- 例如片键是
{tenantId: 1, createdAt: -1},那么{tenantId: 1, email: 1}可建唯一索引,但{email: 1}或{createdAt: -1, email: 1}都不行 - 即使使用
sh.shardCollection()后再建索引,也改变不了这个前提
如何验证当前索引是否满足分片唯一要求
先确认片键:
sh.status()
再查集合索引,并重点看 key 字段是否以片键字段开头:
db.users.getIndexes()
- 如果输出中存在
{"key": {"email": 1}, "unique": true},且片键不是email或不含email前缀 → 这个索引无法提供全局唯一保障 - 如果片键是
{orgId: 1},而你看到{"key": {"orgId": 1, "email": 1}, "unique": true}→ 合规,可依赖 - 注意:复合索引中片键字段必须严格出现在最左侧,不能跳过
修复方案:重建符合前缀要求的唯一索引
不能简单 dropIndex + createIndex 就完事。要确保新索引既满足前缀要求,又不破坏现有查询性能。
- 删除旧的无效唯一索引:
db.users.dropIndex({"email": 1}) - 创建带片键前缀的新索引:
db.users.createIndex({"orgId": 1, "email": 1}, {unique: true})(假设片键是orgId) - 如果业务需要单查
email,需额外建一个非唯一索引:db.users.createIndex({"email": 1}),否则该查询会变慢(因无法用上唯一索引的前缀部分) - 后台创建避免阻塞:
db.users.createIndex({"orgId": 1, "email": 1}, {unique: true, background: true})
容易忽略的关键点:应用层仍需兜底
即使索引合规,分片集群下唯一约束仍有边界条件失效风险,比如块迁移期间短暂窗口、或写关注(w)设为 1 时主节点崩溃未同步到仲裁节点。
- 不要把唯一性完全交给索引;关键业务(如注册邮箱)应在应用层加一次
findOne校验 - 插入时捕获
11000 duplicate key错误,而不是静默忽略 - 避免在迁移活跃期执行大批量插入,此时唯一索引校验可能延迟或不一致











