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

Node.js+MongoDB场景下如何处理查询更新同一资源的并发请求问题

问题根因

你遇到的是典型的库存扣减竞态条件问题:两个并发请求几乎同时读取了相同的库存值,在校验阶段都判定库存充足,之后各自执行扣减操作,最终导致库存被扣为负数、超卖。两次点击间隔1秒时功能正常,是因为第一个请求已经完成了库存扣减,第二个请求读取的是更新后的库存值,校验环节就会拦截请求。

你当前的代码逻辑是「先查库存校验,再修改库存」,两个操作拆分且非原子执行,天然存在并发安全漏洞。


修复方案

推荐优先使用数据库原子操作解决,不需要引入额外组件,性能和安全性都足够,方案如下:

核心思路

把「库存校验+库存扣减」两个步骤合并为单次数据库原子操作,利用数据库本身的锁机制保证并发安全,避免先查后改的时间差漏洞。不要在应用层读取库存、计算扣减后的值再全量set回数据库,这种写法无法避免并发覆盖。

代码修改示例

重构deductInventory函数,直接用MongoDB的原子操作符$inc完成扣减,同时在查询条件中加入库存充足的校验:

exports.deductInventory = asyncHandler(async (store, seller, cart) => {
  // 逐个商品执行原子扣减
  for (const item of cart) {
    let query, update;
    if (!item.hasvariation) {
      // 无规格商品扣减
      query = {
        store: store,
        user: seller,
        "products._id": mongoose.Types.ObjectId(item.productId),
        "products.stock": { $gte: item.qty } // 扣减前校验库存足够
      };
      update = { $inc: { "products.$.stock": -item.qty } }; // 原子减库存
      const updateRes = await Inventory.updateOne(query, update);
      if (updateRes.modifiedCount === 0) {
        return { success: false, errors: [`${item.title} 库存不足`] };
      }
    } else {
      // 有规格商品扣减
      query = {
        store: store,
        user: seller,
        "products._id": mongoose.Types.ObjectId(item.productId)
      };
      update = { $inc: { "products.variations.$[variation].options.$[option].stock": -item.qty } };
      // 数组筛选定位到对应规格,同时在筛选条件中校验库存
      const options = {
        arrayFilters: [
          { "variation._id": mongoose.Types.ObjectId(item.variationId) },
          { "option._id": mongoose.Types.ObjectId(item.optionId), "option.stock": { $gte: item.qty } }
        ],
        new: true
      };
      const updateRes = await Inventory.updateOne(query, update, options);
      if (updateRes.modifiedCount === 0) {
        return { success: false, errors: [`${item.title} 库存不足`] };
      }
    }
  }
  return { success: true };
});

原来的单独checkInventory可以保留作为前端预校验使用,正式扣减完全依赖上述原子操作的结果即可。


其他可选方案

如果你的业务场景更复杂,也可以选择以下方案:

  • 乐观锁:给Inventory表加version字段,更新时携带读取时的版本号,只有版本号匹配时才更新成功,失败则直接返回库存不足
  • 分布式锁:用Redis的SETNX对同一商家/同一商品加锁,同一时间只有一个请求能进入库存操作逻辑,操作完成后释放锁

内容的提问来源于stack exchange,提问作者Faris Malik

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.13 17:24:01