You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MongoDB报E11000重复键错误但无重复数据如何排查解决

问题根因

触发E11000重复键错误和你集合里有没有存量重复数据没关系,核心是两个问题:

  1. 并发竞态问题:你用Promise.all搭配map的写法会让所有标签的「校验是否存在-写入」逻辑并行执行。如果传入的tags数组本身有重复值,或者短时间内有相同标签的请求打进来,多个并行任务会同时通过checkTagExist校验(此时新标签还没写入数据库,所有查询都返回不存在),紧接着同时执行写入操作,必然会撞上slug字段的唯一约束。这种场景下写入操作会直接回滚,你事后查库当然看不到重复数据。
  2. 残留无效索引:NestJS+Mongoose的组合不会在你修改Schema定义时自动删除集合上已存在的旧索引。如果你之前给这个集合的其他字段(比如早期版本给name字段加过唯一约束、或者异常生成过其他字段的唯一索引),哪怕现在Schema里只配置了slug的唯一约束,残留的旧索引依然会触发重复键报错。
解决方案

按优先级依次排查修复即可:

  • 第一步先清理无效索引:直连Mongo实例,对目标集合执行db.你的集合名.getIndexes(),把所有和当前Schema定义不匹配的唯一索引全部删除,删完重启服务再验证。
  • 第二步修复并发逻辑缺陷,推荐直接改批量实现,性能和稳定性都更好,参考实现如下:
async setTags(tags: Array<string>, tenantId: string): Promise<Array<string>> {
  // 前置过滤空值、本地去重,从源头减少无效请求
  const validTags = [...new Set(tags.filter(tag => tag && tag.trim()))];
  if (validTags.length === 0) return [];

  // 一次性批量查询所有已存在的标签,避免循环内单条查询的性能损耗和竞态窗口
  const existTags = await this.tagsModel.find({ name: { $in: validTags } });
  const existTagIdMap = new Map(existTags.map(tag => [tag.name, tag._id]));
  const pendingCreateTags = validTags.filter(name => !existTagIdMap.has(name));

  // 批量插入不存在的标签,配置ordered:false做兜底,遇到重复键错误时跳过冲突项、返回已插入成功的文档
  const createdTags = pendingCreateTags.length > 0
    ? await this.tagsModel.insertMany(
        pendingCreateTags.map(name => ({
          name,
          tenantId: 'test-tenantId-01'
        })),
        { ordered: false }
      ).catch(err => err.insertedDocs || [])
    : [];

  // 合并已存在、新创建的标签ID返回
  return [
    ...Array.from(existTagIdMap.values()),
    ...createdTags.map(tag => tag._id)
  ];
}
  • 如果业务场景标签量极小不想重构批量逻辑,也可以把并行执行改成for循环串行执行,保证前一个标签写入完成后再处理下一个,从流程上避免竞态,但这种写法性能差,不推荐高并发场景使用。
  • 额外兜底:配置slug生成规则时增加唯一值校验逻辑,比如生成slug后追加短随机后缀,避免极端并发下的slug碰撞。

内容的提问来源于stack exchange,提问作者Zenixo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 22:54:10