唯一索引是防止重复插入最可靠、最原子的手段,因其由存储引擎层强制执行,不依赖应用逻辑且能抵御并发竞态;常见失效原因包括索引未生效、字段值含隐藏字符、collation未配置、null值处理不当或建索引前存在重复数据。

唯一索引(unique: true)是 MongoDB 防止重复插入最可靠、最原子的手段——它由存储引擎层强制执行,不依赖应用逻辑,也不怕并发竞态。
为什么加了 unique: true 还能插进重复数据?
常见原因不是语法写错,而是索引根本没生效或字段值“看似相同实则不同”:
- 集合已有重复文档,建索引时直接失败(
duplicate key错误),但你没检查返回值或日志 - 字段值含不可见字符:末尾空格、全角/半角空格、换行符、
\u200b零宽空格等 - 大小写不敏感需求下没配
collation,导致"Alice"和"alice"被视为不同值 - 字段允许
null,而 MongoDB 默认把多个null当作互不冲突(需改用sparse: true)
建索引前务必先查:db.users.find({ email: { $exists: true } }).distinct("email"),确认实际值分布;建完立刻验证:db.users.getIndexes(),确保状态为 "ready" 且 unique: true 出现在输出中。
用户名和邮箱要分别去重,该用复合索引还是单字段索引?
必须用两个独立的单字段唯一索引,不能合并成一个复合索引。
错误写法(无法防止单字段重复):db.users.createIndex({ username: 1, email: 1 }, { unique: true })
正确写法(各守一关):
db.users.createIndex({ "username": 1 }, { unique: true, name: "username_unique" })
db.users.createIndex({ "email": 1 }, { unique: true, name: "email_unique" })
复合索引只保证 {username:"a", email:"b"} 整体不重复,但完全允许 {username:"a", email:"c"} 和 {username:"d", email:"b"} 同时存在——这正是你要阻止的业务场景。
Go / Node.js 等应用中如何安全处理唯一索引冲突?
不要前置查一遍再插入(find + insert),那是竞态高发区。应该直接插入,捕获并解析错误:
- Go(mongo-go-driver):检查
writeException.WriteErrors[0].Code == 11000,再读writeException.WriteErrors[0].KeyPattern判断是username还是email冲突 - Node.js(mongodb driver):
err.code === 11000,然后从err.errmsg中提取字段名(如"username_1 dup key") - Python(pymongo):
exc.code == 11000,配合exc.details["keyPattern"]精准定位
注意:Driver 默认可能重试写入(尤其副本集场景),若业务要求“首次失败即返回”,需显式关闭 retryWrites=false 或在错误处理中区分重试与真实冲突。
高并发下仅靠 Upsert 不够,为什么必须搭配唯一索引?
upsert 本身不防重——它只是“查无则插,有则更新”。两个并发请求同时查不到记录,就会都执行插入,造成重复。
唯一索引才是兜底防线:哪怕两个请求都走到插入阶段,第二个一定会被存储引擎拦截,抛出 11000 duplicate key 错误,此时应用可重试、降级或提示用户。
真正健壮的流程是:先建好 username 和 email 的唯一索引 → 应用直插 → 捕获 11000 错误 → 按字段返回具体提示(如“该邮箱已被注册”)。所有前置校验、缓存判断、应用层锁,都只是优化手段,不能替代索引这一层原子约束。











