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

Express.js余额扣减并发请求致余额为负问题及原子更新解决方案

并发余额扣减漏洞解决方案

根本原因

当前代码逻辑是「先读取余额→后端做校验→再修改余额→保存」的分步操作,并发场景下多个请求会同时拿到相同的余额快照,全部通过校验后依次执行扣减,最终导致余额被扣为负数。

修复方案

方案1:使用MongoDB原子操作(最优)

将余额校验和扣减合并为单次原子更新操作,利用数据库的行锁机制保证同一时间只有一个更新请求能生效,避免竞态条件。核心使用$inc操作符做增量扣减,同时把余额校验放到更新条件里:

const body = req.body
const userId = req.userAuth
const cartId = req.params.id

// 1. 先查询购物车,和原有逻辑一致
const Cart = await Model.Cart.findOne({
    _id: cartId,
    user_id: userId,
    status: 'UNPAID'
})
if (!Cart) {
    return res.status(404).json({ message: '无效的购物车记录' })
}
const payAmount = Cart.product.price

let updateCondition = { _id: userId }
let updateContent = {}
let errorMsg = ''
if (body.payment_method === 'BALANCE') {
    // 余额大于等于支付金额作为更新条件
    updateCondition.balance = { $gte: payAmount }
    // 原子扣减余额
    updateContent.$inc = { balance: -payAmount }
    errorMsg = 'Not Enough Balance'
} else if (body.payment_method === 'POIN') {
    updateCondition.poin = { $gte: payAmount }
    updateContent.$inc = { poin: -payAmount }
    errorMsg = 'Not Enough Poin'
}

// 2. 执行原子更新,new: true表示返回更新后的用户数据
const updatedUser = await Module.User.findOneAndUpdate(
    updateCondition,
    updateContent,
    { new: true }
)

// 3. 没有返回更新结果说明校验没通过,直接返回错误
if (!updatedUser) {
    return res.status(422).json({ message: errorMsg })
}

// 4. 补录余额/积分前后值
if (body.payment_method === 'BALANCE') {
    updatedUser.balance_before = updatedUser.balance + payAmount
    updatedUser.balance_after = updatedUser.balance
} else if (body.payment_method === 'POIN') {
    updatedUser.poin_before = updatedUser.poin + payAmount
    updatedUser.poin_after = updatedUser.poin
}
await updatedUser.save()

// 5. 后续再更新购物车状态、生成交易记录即可

补充防护方案

  • 购物车状态锁:在处理支付请求前,先把购物车状态从UNPAID原子更新为PROCESSING,后续请求查询到状态为处理中就直接拒绝,避免同一个购物车被多次支付
  • 幂等校验:为每个支付请求生成唯一的请求号,同一个请求号只能处理一次,防止用户重复点击或重试导致的重复扣减
  • 分布式锁:针对用户ID加锁,同一时间只能有一个该用户的支付请求在处理,适合复杂的多步交易场景

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.29 12:36:06