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

基于Node.js/MongoDB的金融应用并发取款超余额问题求助

并发取款超余额问题的解决方案

我维护着一个基于node.js、mongodb和mongoose(版本^5.9.9)开发的金融应用,目前在取款逻辑上遇到并发问题:当用户同时多次调用取款接口时,可能取出超过账户余额的金额。比如账户余额10美元时,并发发起多次10美元取款请求居然都能成功。

我知道问题出在并发操作里,读取user.balance的动作早于扣款更新,导致所有并发请求读到的都是初始余额10美元。作为后端新手,术语可能用得不对,请见谅。

我的初始代码:

try{
  // 执行账号验证等操作

  const user = await User.findById(req.user._id);

  if(user.balance < req.body.amount)
    return res.status(400).send({message:"余额不足"});

   // 将数据库中用户余额更新为 user.balance - req.body.amount

   return res.status(200).send({message:"资金已转入指定账户"})
}

之后我尝试过先更新用户记录再校验余额,但不确定能不能彻底解决问题,有没有漏洞,或者有没有更好的办法。我的第二次尝试代码:

try{

  // 执行账号验证等操作
  const idx = uuid()

  const user = await User.findByIdAndUpdate(req.user._id, {withdrawal_intent:idx}, {
       new: true,
  });


  if(user.balance < req.body.amount)
    return res.status(400).send({message:"余额不足"});

   // 将数据库中用户余额更新为 user.balance - req.body.amount

   return res.status(200).send({message:"资金已转入指定账户"})
}

我愿意重构代码采用最佳方案。


核心解决方案:数据库原子操作

你的两次尝试都没解决根本问题——都是先读取余额再更新,并发场景下依然会出现多个请求同时读到旧余额的情况。必须让余额校验和扣款操作在数据库层面原子完成,才能彻底避免超支。

1. 原子更新实现代码(推荐)

利用MongoDB的findOneAndUpdate,将余额校验条件和扣款操作合并为一个原子步骤,数据库会保证同一时间只有一个请求能满足条件并执行更新:

try {
  // 前置操作:账号验证等逻辑

  const withdrawalAmount = req.body.amount;
  
  // 原子操作:仅当余额 >= 取款金额时,才执行扣减
  const updatedUser = await User.findOneAndUpdate(
    {
      _id: req.user._id,
      balance: { $gte: withdrawalAmount } // 数据库层面校验余额足够
    },
    { $inc: { balance: -withdrawalAmount } }, // 原子扣减余额
    { new: true, runValidators: true } // 返回更新后的文档,启用字段验证
  );

  if (!updatedUser) {
    // 没有匹配到符合条件的文档,说明余额不足
    return res.status(400).send({ message: "余额不足" });
  }

  // 后续操作:比如调用第三方转账接口、记录流水等

  return res.status(200).send({ message: "资金已转入指定账户" });
} catch (err) {
  res.status(500).send({ message: "服务器错误", error: err.message });
}

2. 可选增强:乐观锁控制并发

如果需要更严格的并发冲突处理,可以给用户文档添加版本号,配合乐观锁避免冲突:

第一步:修改User模型

const mongoose = require('mongoose');
const { Schema } = mongoose;

const userSchema = new Schema({
  // 原有字段...
  balance: { type: Number, default: 0, min: 0 },
  __v: { type: Number, default: 0 } // 版本号字段,Mongoose默认自带,没有则手动添加
});

// 启用Mongoose乐观锁插件(需先安装:npm install mongoose-optimistic-concurrency)
userSchema.plugin(require('mongoose-optimistic-concurrency'));

const User = mongoose.model('User', userSchema);

第二步:带乐观锁的取款逻辑

try {
  const withdrawalAmount = req.body.amount;
  const user = await User.findById(req.user._id);

  if (user.balance < withdrawalAmount) {
    return res.status(400).send({ message: "余额不足" });
  }

  // 仅当文档版本号未被修改时,才执行扣款
  const updatedUser = await User.findOneAndUpdate(
    {
      _id: req.user._id,
      __v: user.__v // 匹配当前读取到的版本号
    },
    { 
      $inc: { balance: -withdrawalAmount },
      $inc: { __v: 1 } // 更新后版本号自增
    },
    { new: true }
  );

  if (!updatedUser) {
    // 版本号不匹配,说明有其他并发操作修改了文档,返回冲突提示
    return res.status(409).send({ message: "操作冲突,请稍后重试" });
  }

  return res.status(200).send({ message: "资金已转入指定账户" });
} catch (err) {
  res.status(500).send({ message: "服务器错误", error: err.message });
}

为什么你的第二次尝试无效?

你更新withdrawal_intent的操作和余额校验依然是分离的,并发场景下多个请求还是会同时读取到旧余额,本质上还是先读后写的非原子操作,无法解决根本问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 09:15:16