使用Mongoose事务处理findOneAndUpdate(带upsert)的并发请求问题
问题与解答
问题描述
前端通过AJAX向后端发送任务名称,规则为:若任务名称为新则添加至集合,已存在则返回422状态码。当前技术栈为Node.js + MongoDB + Mongoose,原代码采用findOneAndUpdate(带upsert)实现,但担心并发请求下会创建多个同名任务,遂改用事务处理。现咨询两个问题:
- 关于并发请求的问题考虑是否正确?
- 该事务解决方案是否可行?
原实现代码
const createTask = async (req, res) => { let resQuery = await taskModel.findOneAndUpdate( { name: req.body.name }, { $setOnInsert: { name: req.body.name } }, { new: true, upsert: true, includeResultMetadata: true, sanitizeFilter: true, } ); //updateExisting is false if the document was already present if (resQuery.lastErrorObject.updatedExisting === true) { res.status(422).json({ success:false , data: "Task already exists" }); } else { const { name, completed } = resQuery.value; res.status(201).json({ name, completed }); } };
事务处理代码
const createTask = async (req, res) => { let resQuery; const session = await mongoose.startSession(); try { await session.startTransaction(); resQuery = await taskModel .findOneAndUpdate( { name: req.body.name }, { $setOnInsert: { name: req.body.name } }, { new: true, upsert: true, includeResultMetadata: true, sanitizeFilter: true, } ) .session(session); if (resQuery.lastErrorObject.updatedExisting) { throw new ErrorRequest(STATUS_CODE.BAD_REQUEST, "User already exists"); } await session.commitTransaction(); session.endSession(); const { name, completed } = resQuery.value; res.status(201).json({ name, completed }); } catch (err) { await session.abortTransaction(); session.endSession(); //Here I could call next(err) to handle all error in the same place res.status(422).json({ error: "Task already exists", data: req.body }); } };
问题解答
1. 并发请求问题的考虑是否正确?
你的担心是多余的。MongoDB的findOneAndUpdate(带upsert: true)本身就是原子操作:当多个请求同时尝试插入同名任务时,MongoDB会自动保证只有第一个请求能成功插入新文档,后续请求会触发更新逻辑(但因为你用了$setOnInsert,不会修改现有文档),且updatedExisting会返回true。原代码本身就不会出现并发创建多个同名任务的情况,完全不需要额外处理并发。
2. 事务解决方案是否可行?
这个方案可行但完全没必要,甚至会带来额外性能开销:
- 事务会占用更多数据库资源、增加操作延迟,对于这种单文档的简单操作属于过度设计。
- 你的事务代码逻辑和原代码没有本质区别,只是把
findOneAndUpdate套进了事务,但这个操作本身已经是原子的,事务没解决任何实际问题。
另外注意:事务代码里的错误信息写了"User already exists",和业务场景的"Task"不符,属于笔误,需要修正。
更优方案
最稳妥的方式是在taskModel的name字段上创建唯一索引,让数据库从根源上阻止重复数据插入,代码逻辑也会更简洁:
// 定义Task模型时添加唯一索引 const taskSchema = new mongoose.Schema({ name: { type: String, unique: true, // 唯一约束 required: true }, completed: { type: Boolean, default: false } }); // 创建任务的处理代码 const createTask = async (req, res) => { try { const task = await taskModel.create({ name: req.body.name }); res.status(201).json({ name: task.name, completed: task.completed }); } catch (err) { // 捕获唯一键冲突错误(错误码11000) if (err.code === 11000) { return res.status(422).json({ success: false, data: "Task already exists" }); } // 处理其他服务器错误 res.status(500).json({ success: false, data: "Server error" }); } };
这个方案的优势:
- 数据库层面的唯一约束是最可靠的,彻底避免重复数据。
- 代码逻辑更直观,不需要依赖
findOneAndUpdate的返回值判断,直接通过创建操作和错误捕获实现需求。
内容的提问来源于stack exchange,提问作者Nick
相关产品推荐
相关产品推荐

