TypeScript Node操作MongoDB重复插入6条数据未执行更新的问题
问题成因
- 异步遍历写法错误:
forEach不支持等待异步回调执行,即使回调内加了await,forEach也会直接发起所有遍历项的逻辑,不会等上一项的数据库操作完成再执行下一项。你写出6条重复数据,刚好对应fields数组长度为6的场景:6次存在性检查几乎同时发送到数据库,此时第一条插入还没完成,所有检查都返回null,各自触发插入逻辑。 - 判断分支无互斥:
checksIfExists == null的插入逻辑执行完后,没有中断流程,会继续往下执行后续独立的if/else判断,单次遍历就可能触发2次插入。 - 非原子操作风险:应用层实现「先查再写」的逻辑天然存在并发缺陷,检查操作和写入操作之间有时间差,高并发下必然出现重复写入。
- 无效资源开销:每次循环都新建
DocumentDbRepository数据库连接实例,平白增加连接开销,还会放大并发问题。 - 变量污染:else分支直接对入参
information重新赋值为数组,会导致后续遍历拿到的information结构异常,触发非预期逻辑。
修复方案
方案1:最小改动修复(适配现有代码结构)
仅修正现有逻辑的显性错误,不做架构调整:
- 将
forEach替换为支持异步等待的for...of循环,保证遍历项串行执行 - 把数据库连接实例的创建移到循环外,复用同一个连接
- 补充分支互斥逻辑,记录不存在插入完成后直接进入下一轮循环,不执行后续判断
- 不直接修改原始入参对象,写入操作使用拷贝的新对象,避免变量污染
修复后代码:
// 循环外创建连接实例,复用连接 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
相关产品推荐
相关产品推荐

