MongoDB 唯一索引如何保证唯一性?Duplicate Key 错误怎么处理?
简化版
MongoDB 唯一索引通过索引键约束字段组合不能重复,重复写入会报 Duplicate Key 错误。业务上要把它当作并发唯一性的最终防线,而不是只依赖应用层先查再插。
详细版
唯一索引用于保证某个字段或字段组合在集合内唯一,比如用户名、订单号、租户内编码。
- 可以建单字段唯一索引,也可以建复合唯一索引。
- 并发场景下“先查不存在再插入”不可靠,必须用唯一索引兜底。
- Duplicate Key 错误应该被业务识别并转成可理解响应,如“用户名已存在”。
- 对可选字段、软删除字段,要结合 partial index 设计,否则可能和预期不一致。
- 分片集合上的唯一约束还要考虑 shard key 规则,不能简单照搬单机思路。
完整版教学
一、为什么唯一性必须落到数据库层
应用层检查唯一性有天然竞态。两个请求同时注册同一个用户名,都先查询“用户名不存在”,然后同时插入,如果数据库没有唯一约束,两条重复记录就可能出现。唯一索引把“检查是否重复”和“写入索引键”放到数据库内部完成,能在并发下保证最终只有一个成功。它是业务唯一性的最后防线,尤其适合用户名、手机号、订单号、支付流水号这类关键字段。
db.users.createIndex({ username: 1 }, { unique: true })
记忆钩子:应用层校验是用户体验,唯一索引才是并发安全的闸门。
二、复合唯一索引表达的是字段组合唯一
单字段唯一表示这个字段全集合唯一,复合唯一表示字段组合唯一。例如 SaaS 系统里,用户编码可能只要求在同一个租户内唯一,那么应该建 { tenantId: 1, code: 1 } 的唯一索引,而不是只给 code 建唯一索引。这样租户 A 和租户 B 都可以有 admin,但同一个租户内不能重复。面试回答时要把“业务唯一范围”说清楚,因为很多唯一性不是全局唯一。
db.members.createIndex(
{ tenantId: 1, username: 1 },
{ unique: true }
)
三、Duplicate Key 错误应该怎么处理
当插入或更新违反唯一索引时,MongoDB 会返回 Duplicate Key 错误。应用不应该把数据库错误原样抛给用户,而要识别错误码和索引名,转成业务错误。比如注册时返回“用户名已存在”,导入数据时标记具体重复行。对于接口幂等场景,还可以在 Duplicate Key 后查询已有记录,判断它是否就是同一个请求创建的结果。
try {
await users.insertOne({ username: "alice" })
} catch (e) {
if (e.code === 11000) {
throw new Error("用户名已存在")
}
throw e
}
四、带数字理解并发竞态
假设两个注册请求间隔只有 3ms。请求 A 在 T0 查询用户名不存在,请求 B 在 T1 也查询不存在,A 在 T2 插入,B 在 T3 插入。如果没有唯一索引,两个都成功;如果有唯一索引,B 的插入会在索引维护阶段失败。这个例子说明,先查再插只能改善提示,不能保证并发正确性。
T0 A: find username=alice -> none
T1 B: find username=alice -> none
T2 A: insert alice -> success
T3 B: insert alice -> duplicate key
五、可选字段和软删除要特别设计
如果字段不是每条文档都有,直接建唯一索引可能和预期不同。比如很多用户暂时没有邮箱,但你又希望有邮箱的人邮箱唯一,就要考虑 sparse 或 partial unique index。软删除也类似:如果删除记录仍留在集合里,普通唯一索引会阻止新记录使用同一个业务 key;这时可以只对 deleted:false 的文档建立部分唯一索引。这里的关键是唯一约束的范围必须和业务语义一致。
| 场景 | 常见索引 |
|---|---|
| 用户名全局唯一 | { username:1 } unique |
| 租户内唯一 | { tenantId:1, username:1 } unique |
| 未删除数据唯一 | partial unique with deleted:false |
| 可选邮箱唯一 | partial unique with email exists |
六、分片集合里的唯一索引注意点
在分片集群中,唯一性校验要考虑数据分布。如果唯一索引不包含 shard key,数据库可能无法在单个 shard 上完成唯一判断,规则和限制会更复杂。直观理解是:数据分散在多个 shard 上,想保证全局唯一,就必须有办法把相同唯一键路由到可校验的位置。生产设计分片集合唯一约束时,要提前查版本规则并把 shard key 和业务唯一键一起设计,不能后期再补。
如果按 tenantId 分片:
{ tenantId, username } 唯一更自然
只要求 username 全局唯一则要额外设计路由和约束策略
七、常见误区与追问
- 误区:先查再插就能保证唯一。 并发下两个请求都可能查到不存在,唯一索引才是最终防线。
- 误区:Duplicate Key 是系统异常不用处理。 它常常是可预期业务冲突,应该转成友好提示或幂等响应。
- 误区:复合唯一索引表示每个字段都分别唯一。 它约束的是字段组合,不是单个字段各自唯一。
- 追问:软删除后如何允许同名新记录? 可以对
deleted:false建 partial unique index。 - 追问:分片集合唯一索引为什么复杂? 因为数据分布在多个 shard,唯一校验必须和 shard key 路由能力匹配。
八、加强记忆
唯一索引可以记成“数据库层的唯一闸门”。应用先查是为了给用户好提示,真正挡住并发重复的是唯一索引;Duplicate Key 不是脏错误,而是业务冲突信号。回答时把单字段、复合范围、软删除 partial unique、分片限制这几层讲出来,就能覆盖从入门到生产的关键点。