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
相关产品推荐
相关产品推荐

