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

Node.js并发/并行调用API时MongoDB生成重复条目如何解决?

解决方案

1. 数据库层加唯一约束(最核心,必做)

这是从根源上避免重复写入的方案,优先级最高,因为数据库的唯一索引校验是原子性的,不受业务代码并发逻辑影响。
根据你的业务场景,给需要保证唯一性的字段创建唯一索引,比如如果要求Account的name不能重复,执行MongoDB命令:
db.accounts.createIndex({ "name": 1 }, { unique: true })
如果你的业务规则是一个手机号对应唯一账号,就创建复合唯一索引:
db.accounts.createIndex({ "name": 1, "phoneNumber": 1 }, { unique: true })
加完索引后,你需要在代码里捕获唯一键冲突的错误(MongoDB错误码为11000),直接返回用户已存在的提示即可,代码调整示例:

try {
  // 可以保留原有查询逻辑做前置拦截,减少数据库写入冲突概率
  const userPresent = await User.findOne({
    phoneNumber: data.phoneNumber,
  });
  if (userPresent) {
    throw new CustomError("OIC_ERROR_00027", "User already present");
  }
  const newAccount = await new Account({
    name: data.name,
    phoneNumber: data.phoneNumber
  }).save();
} catch (err) {
  if (err.code === 11000) {
    throw new CustomError("OIC_ERROR_00027", "User already present");
  }
  throw err;
}

你原来的先查再插逻辑属于「非原子操作」,并发场景下必然会出现时间差导致的重复写入,单独使用完全不可靠,需搭配约束方案使用。

2. 改用原子写入操作

把查询和写入合并为单个原子操作,直接用MongoDB的upsert逻辑,不需要分开执行查询和插入,从业务操作层面消除并发时间差:

// 不存在就插入,存在就返回已有文档,全程原子操作
const account = await Account.findOneAndUpdate(
  { phoneNumber: data.phoneNumber }, // 查询条件
  {
    $setOnInsert: {
      name: data.name,
      phoneNumber: data.phoneNumber
      // 其他新建文档时需要写入的字段
    }
  },
  {
    upsert: true, // 匹配不到文档时自动执行插入
    new: true, // 返回更新/插入后的文档
    setDefaultsOnInsert: true // 插入时自动应用Schema默认值
  }
);

该方案可以单独使用,也可以和唯一索引搭配实现双重防护。

3. 业务层前置拦截(降低触发概率)

  • 前端层面:用户点击提交按钮后立即置灰,短时间内禁止重复点击,拦截普通用户误操作导致的重复请求,能解决90%以上的常规场景问题。
  • 接口层面:增加幂等校验,用户进入提交页时后端生成唯一幂等Token存入缓存,提交请求时必须携带该Token,后端先校验Token是否存在,已使用过的Token直接返回重复提交提示,未使用的标记为已使用后再执行业务逻辑,可解决网络重发、多设备同时提交的问题。

4. 分布式锁(补充防护)

如果是分布式部署的服务,可借助Redis的SETNX命令实现分布式锁,处理请求前先尝试加锁,锁的过期时间设置为略长于接口最长处理时间,拿不到锁直接返回操作频繁提示,处理完成后释放锁。注意该方案属于补充防护,不能替代数据库唯一索引,避免锁失效时出现脏数据。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 07:06:03