Express.js、Node.js、Mongoose瓶数计数接口的并发请求数据异常问题
并发请求导致Mongoose数据异常(amount变为负数)的原因与解决方案
问题回顾
你用Express.js、Node.js和Mongoose开发了一个瓶数计数项目,/give接口的逻辑是找到type: "coca"的文档,检查amount是否大于0,若满足则减1后保存。当你把数据库中amount设为1,然后快速发起10次请求时,出现了服务器响应5次成功、5次失败,但最终数据库amount变成-5的异常情况。
为什么会出现这个问题?
这完全是**竞态条件(Race Condition)**导致的!咱们拆解下并发请求的执行流程:
- 10个请求几乎同时到达服务器,每个请求都执行
Bottle.findById({type: "coca"})。 - 因为请求太密集,大部分请求在数据库还没来得及更新
amount时,都读取到了初始值1。 - 每个读取到
1的请求都会跳过amount <=0的判断,执行data.amount -=1,然后调用data.save()。 - 第一个请求保存后,
amount变成0,但其他已经拿到初始值1的请求并不会重新查询最新值,而是继续把自己内存中的data.amount改成0并保存——这时候数据库的amount会被覆盖成0。 - 后续的请求中,有的可能读取到了
0,返回失败;但还有一部分请求已经在之前拿到了1,继续执行减1操作,把amount改成-1、-2……直到最后变成-5。
简单来说:你的代码里「查询→修改→保存」是三个独立的非原子操作,并发场景下多个请求会基于同一个旧值修改,最终覆盖彼此的结果,完全忽略了中间的判断逻辑。
怎么解决这个问题?
最可靠的方案是让数据库层面执行原子操作,把「检查+修改」合并成一个数据库能保证原子性的操作,彻底消除竞态条件。这里有两种常用的实现方式:
方案1:使用Mongoose的updateOne原子更新(推荐)
直接用MongoDB的$inc操作符和条件匹配,让数据库一次性完成「检查amount>0」和「减1」的操作:
app.post("/give", async function(req, res) { // 原子操作:匹配type为coca且amount>0的文档,将amount减1 const updateResult = await Bottle.updateOne( { type: "coca", amount: { $gt: 0 } }, { $inc: { amount: -1 } } ); // modifiedCount表示实际被修改的文档数量 if (updateResult.modifiedCount === 0) { return res.json({ message: "Not enough coca" }); } return res.json({ message: "Give success" }); });
这个方法的优势:
- 所有操作在数据库层面原子执行,不管多少并发请求,都不会出现竞态。
- 性能更高,减少了「查询→保存」两次数据库往返。
- 天然保证
amount不会变成负数,因为只有amount>0的文档才会被修改。
方案2:使用MongoDB事务(适合复杂业务场景)
如果你的业务逻辑比这个例子更复杂,需要先查询文档做更多处理,再保存,可以用MongoDB事务(注意:事务需要MongoDB是副本集或分片集群,单节点不支持):
app.post("/give", async function(req, res) { let session = await mongoose.startSession(); session.startTransaction(); try { // 在事务中查询文档,保证后续操作的隔离性 const bottle = await Bottle.findOne({ type: "coca" }).session(session); if (bottle.amount <= 0) { await session.abortTransaction(); session.endSession(); return res.json({ message: "Not enough coca" }); } bottle.amount -= 1; await bottle.save({ session }); await session.commitTransaction(); session.endSession(); return res.json({ message: "Give success" }); } catch (err) { await session.abortTransaction(); session.endSession(); return res.json({ message: "Failed to give coca", error: err.message }); } });
事务会把整个「查询→修改→保存」的流程变成一个原子操作,避免其他请求在中间修改数据。
总结
你的核心问题是没有处理并发场景下的竞态条件,把非原子的「查询-修改-保存」改成数据库级的原子操作,就能彻底解决数据异常的问题。其中updateOne的原子更新是最简单高效的方案,优先推荐。
内容的提问来源于stack exchange,提问作者Trọng Nguyễn Công
相关产品推荐
相关产品推荐

