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

Node.js+Mongoose构建充值RESTful API遇并发扣减金额异常问题

解决Mongoose并发扣减账户金额的竞态问题

嘿,这个并发扣减金额的坑我之前踩过!你遇到的问题本质是竞态条件——现在的代码是先查询账户余额,再判断、扣减,这几步不是原子操作,高并发下多个请求会同时查到余额还够,然后都执行扣减,最后就把余额搞成负数了,完全绕过了你写的user.money <=0判断。

核心问题分析

你当前的代码流程是:

  1. 通过Bottle.findById查询用户账户
  2. 判断余额是否足够
  3. 修改余额并保存

这三步在单请求下没问题,但并发时,多个请求会同时停在步骤2,都认为余额足够,接着都执行步骤3,最终导致余额低于0。

最优解决方案:使用MongoDB原子更新操作

MongoDB支持原子性的更新操作,我们可以把“余额判断+扣减”合并成一个原子操作,让数据库层面保证操作的唯一性,彻底避免竞态条件。

推荐用findOneAndUpdate来实现,示例代码如下:

app.post("/recharge", async function(req, res) {
  try {
    // 原子操作:同时查询符合条件的用户并扣减金额
    const updatedUser = await Bottle.findOneAndUpdate(
      { 
        user: "foo", 
        money: { $gte: 50 } // 前置条件:余额至少有50才能扣减
      },
      { $inc: { money: -50 } }, // 原子扣减50
      { new: true } // 返回更新后的用户文档
    );

    if (!updatedUser) {
      // 没有找到符合条件的用户(余额不足)
      return res.json({ message: "Not enough money" });
    }

    res.json({ message: "Deduction successful", user: updatedUser });
  } catch (err) {
    res.status(500).json({ message: "Server error", error: err.message });
  }
});

这个操作的优势在于:查询和更新是MongoDB内部的原子操作,多个并发请求过来时,数据库会排队处理,只有第一个满足money >=50的请求能成功扣减,其他请求会因为条件不匹配返回null,从根源上杜绝了余额负数的情况。

复杂场景备选:乐观锁

如果你的业务逻辑更复杂(比如扣减前需要做其他计算),原子更新满足不了需求,可以试试乐观锁方案:给账户文档加一个version字段,每次更新时验证版本号,只有版本匹配才允许更新。

示例代码:

app.post("/recharge", async function(req, res) {
  const session = await Bottle.startSession();
  session.startTransaction();
  
  try {
    // 开启事务查询用户
    let user = await Bottle.findOne({ user: "foo" }).session(session);
    
    if (!user || user.money < 50) {
      await session.abortTransaction();
      return res.json({ message: "Not enough money" });
    }

    // 扣减金额并更新版本号
    user.money -= 50;
    user.version += 1;

    // 保存时验证版本号,只有版本匹配才更新
    const updatedUser = await user.save({ 
      session,
      query: { version: user.version - 1 } // 确保是当前版本的文档
    });

    if (!updatedUser) {
      await session.abortTransaction();
      return res.json({ message: "Request conflict, please try again" });
    }

    await session.commitTransaction();
    res.json({ message: "Deduction successful", user: updatedUser });
  } catch (err) {
    await session.abortTransaction();
    res.status(500).json({ message: "Server error", error: err.message });
  } finally {
    session.endSession();
  }
});

这种方式下,并发请求修改同一个文档时,只有第一个能成功,其他会因为版本号不匹配失败,你可以让前端重试或者返回冲突提示。

总结

  • 简单的金额扣减场景,优先用原子更新操作,高效且可靠;
  • 复杂业务场景再考虑乐观锁或MongoDB事务(事务适合多文档操作的情况)。

内容的提问来源于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:24:22