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

TypeScript Node操作MongoDB重复插入6条数据未执行更新的问题

问题成因
  • 异步遍历写法错误:forEach不支持等待异步回调执行,即使回调内加了await,forEach也会直接发起所有遍历项的逻辑,不会等上一项的数据库操作完成再执行下一项。你写出6条重复数据,刚好对应fields数组长度为6的场景:6次存在性检查几乎同时发送到数据库,此时第一条插入还没完成,所有检查都返回null,各自触发插入逻辑。
  • 判断分支无互斥:checksIfExists == null的插入逻辑执行完后,没有中断流程,会继续往下执行后续独立的if/else判断,单次遍历就可能触发2次插入。
  • 非原子操作风险:应用层实现「先查再写」的逻辑天然存在并发缺陷,检查操作和写入操作之间有时间差,高并发下必然出现重复写入。
  • 无效资源开销:每次循环都新建DocumentDbRepository数据库连接实例,平白增加连接开销,还会放大并发问题。
  • 变量污染:else分支直接对入参information重新赋值为数组,会导致后续遍历拿到的information结构异常,触发非预期逻辑。
修复方案

方案1:最小改动修复(适配现有代码结构)

仅修正现有逻辑的显性错误,不做架构调整:

  1. 将forEach替换为支持异步等待的for...of循环,保证遍历项串行执行
  2. 把数据库连接实例的创建移到循环外,复用同一个连接
  3. 补充分支互斥逻辑,记录不存在插入完成后直接进入下一轮循环,不执行后续判断
  4. 不直接修改原始入参对象,写入操作使用拷贝的新对象,避免变量污染

修复后代码:

// 循环外创建连接实例,复用连接
const mongoConnection = new DocumentDbRepository();
// 用for...of替代forEach,支持await串行执行
for (const value of fields) {
  try {
    const checksIfExists = await mongoConnection.getValue(key, information[uniqueValue]);

    if (checksIfExists == null) {
      console.log('---------首次插入---------');
      const sons = Object.values(information.ticketToRide);
      // 不修改原始入参,生成新的写入对象
      const insertData = { ...information, ticketToRide: sons };
      const insertRes = await mongoConnection.insertValue(insertData);
      console.log(insertRes);
      // 插入完成直接进入下一轮循环,不走后续分支
      continue;
    }

    if (checksIfExists.passValue === information.passValue) {
      console.log('---------update---------');
      const sons = Object.values(information.ticketToRide);
      const updateData = { ...information, ticketToRide: sons };
      const updateRes = await mongoConnection.updateRegister(updateData, checksIfExists._id);
      console.log(updateRes);
    } else {
      console.log('---------条件不匹配插入---------');
      const sons = Object.values(information.ticketToRide);
      const insertData = { ...information, ticketToRide: sons };
      const insertRes = await mongoConnection.insertValue(insertData);
      console.log(insertRes);
    }
  } catch (error) {
    console.log(error);
  }
}

方案2:最优修复(原子操作彻底规避并发问题)

应用层做「先查再写」永远存在时间差,最稳妥的方案是直接使用MongoDB原生支持的upsert原子操作,把「检查存在、更新、插入」三个动作放到数据库端一次性完成,从根上避免并发导致的重复写入。
首先给DocumentDbRepository类新增原子upsert方法:

async upsertRegister(uniqueKey: string, keyValue: any, information: any) {
  if (this._connection == null) {
    await this.connect();
  }
  const db = this._connection.db(DocumentDbRepository.DbName);
  const ticketToRide = db.collection("ticketToRide");
  // 构造唯一查询条件
  const query = { [uniqueKey]: String(keyValue) };
  // 处理写入数据结构,不修改原始入参
  const sons = Object.values(information.ticketToRide);
  const writeData = { ...information, ticketToRide: sons };

  // 原子执行:匹配到记录则更新,匹配不到则插入
  const result = await ticketToRide.findOneAndUpdate(
    query,
    { $set: writeData },
    {
      upsert: true,
      returnDocument: 'after'
    }
  );
  return result;
}

业务逻辑可以简化为直接调用原子方法,不需要自己写存在性判断:

const mongoConnection = new DocumentDbRepository();
for (const value of fields) {
  try {
    const res = await mongoConnection.upsertRegister(key, information[uniqueValue], information);
    console.log('操作完成', res);
  } catch (error) {
    console.log(error);
  }
}

额外建议:给做唯一判断的字段加唯一索引,即使代码出现bug,数据库层面也会阻止重复数据插入,是最后一道兜底防线。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 13:15:35