分片集群无法在非分片键字段上创建全局唯一索引,因缺乏跨分片唯一性协调器;唯一索引字段必须是分片键或以其为前缀的复合字段,否则会报错。

在分片集群上无法真正“无锁”创建全局唯一索引。MongoDB 的 createIndex 在 mongos 上执行时,会向所有分片并发构建索引,但唯一性约束要求跨分片验证 —— 这个环节没有分布式唯一性协调器,所以必须依赖分片键参与约束,否则会直接报错 Cannot create unique index with non-unique shard key。
为什么分片集群上不能随便建唯一索引
根本原因在于:分片后数据分散在多个分片上,MongoDB 不会在写入时跨分片查重。如果唯一索引字段 ≠ 分片键(或其前缀),就无法保证全局唯一 —— 比如两个分片各自插入 { email: "a@b.com" },谁都拦不住。
- 唯一索引字段必须是分片键,或是以分片键为前缀的复合字段(例如分片键是
{ userId: 1 },那{ userId: 1, email: 1 }可以建唯一索引,但单独{ email: 1 }不行) - 哈希分片键(如
{ email: "hashed" })本身已隐含唯一性分布意图,此时可直接在email上建唯一索引,因为分片键就是它自己 - 试图绕过限制(比如先建普通索引再改 unique)会失败:MongoDB 拒绝在非分片键字段上设置
unique: true
唯一可行的两种路径
你只有两个实际可落地的选择,取决于当前分片键是否可控:
- 把目标字段加进分片键:用
sh.shardAndDistributeCollection()重建分片(需停写或导出导入),例如从{ _id: "hashed" }改为{ email: "hashed" }或{ email: 1, _id: 1 },之后再运行db.users.createIndex({ email: 1 }, { unique: true }) - 放弃全局唯一索引,改用应用层兜底:在写入前用
findAndModify或两阶段提交模拟“检查-插入”,但要注意网络分区和竞态条件,且无法替代数据库级强约束 - 若已有数据且不能改分片键,唯一补救是全量扫描去重 + 建稀疏唯一索引(
{ email: 1 }+sparse: true),但这只防新数据重复,不解决历史冲突
别信“滚动创建”能避免锁
文档里提的“滚动过程”仅适用于副本集,对分片集群无效。在分片环境下,mongos 发起的 createIndex 会同时在所有分片上启动构建,但只要集合正在被写入,就可能产生违反唯一性的中间状态 —— MongoDB 会拒绝这种风险,强制你停写或重构分片键。
- 即使加了
writeConcern: "majority"或commitQuorum,也不能改变“跨分片唯一性不可验证”这一事实 -
sh.shardAndDistributeCollection()的unique: true参数仅控制底层分片键索引是否带唯一性,不是给业务字段加全局唯一约束 - Atlas 界面点“创建唯一索引”?背后一样校验分片键前缀,不满足就报错,不会静默降级
最常被忽略的一点:很多人以为把索引建在 mongos 就自动全局生效,其实它只是分发命令;真正决定能否加 unique 的,是每个分片本地的索引定义和分片键结构 —— 而这个结构一旦定下,改起来成本远高于早期设计权衡。


















