唯一索引是防重复的底线,不是可选项:不加unique: true仅靠应用层先查后插在高并发下必然失败;MongoDB唯一索引提供原子级兜底,建索引前须清理重复数据,字段值差异(如空格、大小写、null)需用collation或sparse索引处理,建完须验证状态为"ready",并正确捕获11000错误。

唯一索引是防重复的底线,不是可选项
不加 unique: true 索引,仅靠应用层判断(比如先 findOne 再 insertOne)在高并发下必然失败——两个请求几乎同时查不到数据,然后都插入成功。MongoDB 的唯一索引才是原子级兜底。
- 建索引前必须清理已有重复数据,否则
createIndex会直接报错退出 - 字段值看似相同但实际不同(比如
"abc "和"abc"、null多次插入)会导致索引“失效”,需配合collation: { locale: "en", strength: 2 }处理大小写和空格 - 允许字段为
null且业务上要视作“占位”,改用稀疏索引:{ unique: true, sparse: true } - 建完务必执行
db.collection.getIndexes(),确认状态是"ready",别信“命令没报错就成功了”
updateOne + upsert: true 是安全写入的常规姿势
upsert 不是“智能合并”,而是“匹配则更新、不匹配则插入”,但它默认行为是全量替换——如果你传的是普通对象而非 $set 操作符,旧字段会被清空。
- 永远用
{ $set: { field1: val1, field2: val2 } },而不是{ field1: val1, field2: val2 } - 想保留旧值、只更新非空新值?组合
$set和$setOnInsert:{ $set: { updatedAt: new Date() }, $setOnInsert: { createdAt: new Date() } } - 在分片集群中,
filter必须包含完整分片键,否则报Cannot upsert with query that does not contain the shard key - 不要省略
writeConcern,尤其在关键业务中,避免写入成功但未同步到多数节点
为什么你 catch 不到 11000 错误?
很多人加了唯一索引却没处理 11000 duplicate key 错误,结果重复数据被静默忽略或抛出未捕获异常。这不是 MongoDB 的问题,是你没监听它。
- Node.js 驱动中,
insertOne遇到唯一冲突会直接 reject,错误码是11000,错误消息含"dup key" -
updateOne+upsert: true不会触发 11000(它走的是更新路径),所以冲突兜底仍要靠索引 +insertOne或显式try/catch - 别只检查
err.name === "MongoServerError",要深入比对err.code === 11000或正则匹配/E11000|duplicate key/i - 重试逻辑别无脑循环,建议加退避(如
setTimeout)并设最大重试次数,避免雪崩
$addToSet 对象去重基本不可靠
想用 $addToSet 给数组字段去重?对字符串、数字有效;对对象,除非字段顺序、类型、嵌套结构完全一致,否则一律视为不同元素。
- 两个看起来一样的对象
{ tag: "js" }和{ tag: "js" }在 BSON 层可能因字段顺序或类型(NumberIntvsnumber)被判为不等 - 数组越长,
$addToSet性能越差——它得遍历整个数组做全量比较,O(n),且无法利用索引加速 - 真正需要强去重的场景,优先考虑:把对象规整成唯一字符串标识(如
JSON.stringify(obj, Object.keys(obj).sort())),再对该字符串字段建唯一索引 +$addToSet - 或者放弃
$addToSet,改用updateOne+$elemMatch先查是否存在,再$push,虽然多一次查询,但语义可控
upsert 时误传普通对象导致字段丢失——这两点在上线后才暴露,排查成本远高于写代码时多敲两行。


















