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

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)**导致的!咱们拆解下并发请求的执行流程:

  1. 10个请求几乎同时到达服务器,每个请求都执行Bottle.findById({type: "coca"})。
  2. 因为请求太密集,大部分请求在数据库还没来得及更新amount时,都读取到了初始值1。
  3. 每个读取到1的请求都会跳过amount <=0的判断,执行data.amount -=1,然后调用data.save()。
  4. 第一个请求保存后,amount变成0,但其他已经拿到初始值1的请求并不会重新查询最新值,而是继续把自己内存中的data.amount改成0并保存——这时候数据库的amount会被覆盖成0。
  5. 后续的请求中,有的可能读取到了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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.22 09:05:37