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

NodeJS与MongoDB金融项目如何防止并发重复提现?

解决NodeJS+MongoDB金融应用并发提现超余额问题

你遇到的是典型的并发竞态条件问题:当两个提现请求同时发起时,它们都会先读取到相同的账户余额(比如10K),都通过余额校验后,各自执行扣减操作,最终导致账户余额被超额扣除。

下面从数据库和服务器层面给出具体解决方案:

一、数据库层面:原子更新+事务(推荐)

核心思路是让数据库在更新时直接完成余额校验和扣减,避免先读再改的中间竞态窗口,同时用事务保证操作的原子性(要么全部成功,要么全部回滚)。

修改代码步骤:

  1. 替换原有的余额查询校验逻辑,改用MongoDB的findOneAndUpdate做原子操作,确保只有当余额足够时才会执行扣减。
  2. 调整Fawn事务中的余额更新逻辑,基于数据库当前余额计算,而非提前读取的旧值。

修改后的核心代码如下:

//Code checks if the account number is valid
const accountValidation = await handleBankAccountVerification(account_no, bank);
if(accountValidation.status === 'error' || !accountValidation.data) return res.status(400).send(accountValidation);

// 替换原有的余额校验逻辑:原子操作完成校验+扣减
const updatedUser = await User.findOneAndUpdate(
  { _id: req.user._id, balance: { $gte: amount } }, // 条件:用户存在且余额足够
  { $inc: { balance: -amount } }, // 原子扣减余额
  { new: true } // 返回更新后的用户数据
);

if (!updatedUser) {
  return res.status(400).send({ message: "Insufficient balance!" });
}

const payWithManual = async (user, account_name) => {
  try {
    const task = new Fawn.Task();
    const data = req.body;
    data.status = "pending";
    data.balance = updatedUser.balance; // 使用更新后的余额
    data.account_name = account_name || '';
    data.user = req.user._id;

    const { error } = validatePayment(data);
    if (error) return res.status(400).send({ message: error.details[0].message });

    // 事务中不再更新余额(已通过原子操作完成),直接记录支付和交易
    const result = await task
      .save("payments", data)
      .save("transactions", {
        ...transaction,
        action_id: { $ojFuture: "1._id" },
        balance_before: updatedUser.balance + amount, // 反向计算更新前的余额
        balance_after: updatedUser.balance,
      })
      .run({ useMongoose: true });

    // 后续邮件通知逻辑保持不变
    const date = dayjs();
    const payload = { "country": "NG" };
    const response = await flw.Bank.country(payload);
    bankName = response.data.filter(item => item.code === bank);
    
    const html = WithdrawTemplate(amount, updatedUser.balance, date.format("hh:mmA MMM DD, YYYY"), account_no, bankName[0]?.name);
    const adminHtml = WithdrawNotificationTemplate(user.email, amount, updatedUser.balance, date.format("hh:mmA MMM DD, YYYY"), account_no, bankName[0]?.name);
    await user.sendEmailToUser(`Your made a withdraw`, html, user.email);
    await user.sendEmailToUser(`A user is requesting withdrawal`, adminHtml, config.get('NOTIFICATION_EMAIL'));

    return res.status(200).send({ message: "Fund has been sent to account you provided!", result: result[0]._id });
  } catch (e) {
    res.status(400).send(e);
  }
};

const account_name = accountValidation.data.data.account_name;
await payWithManual(updatedUser, account_name);

补充:乐观锁方案(可选)

如果需要更严格的并发控制,可以给用户表加一个version字段,每次更新时校验版本号,避免并发冲突:

// 用户表Schema添加version字段
const userSchema = new mongoose.Schema({
  // 其他字段...
  balance: Number,
  version: { type: Number, default: 0 }
});

// 更新时校验版本号
const updatedUser = await User.findOneAndUpdate(
  { _id: req.user._id, balance: { $gte: amount }, version: user.version },
  { $inc: { balance: -amount, version: 1 } },
  { new: true }
);

如果版本不匹配,说明有并发操作,此时返回错误让用户重试。

二、服务器层面:分布式锁(适用于多实例部署)

如果你的应用是多服务器实例部署,还可以用Redis实现分布式锁,确保同一用户同一时间只有一个提现请求在处理:

const redis = require('redis');
const client = redis.createClient();

// 获取锁,超时时间10秒
const acquireLock = async (userId, timeout = 10000) => {
  const lockKey = `lock:withdraw:${userId}`;
  const result = await client.set(lockKey, 'locked', 'NX', 'PX', timeout);
  return result === 'OK';
};

// 释放锁
const releaseLock = async (userId) => {
  const lockKey = `lock:withdraw:${userId}`;
  await client.del(lockKey);
};

// 在提现接口中使用锁
const isLocked = await acquireLock(req.user._id);
if (!isLocked) {
  return res.status(429).send({ message: "Please wait, your withdrawal is being processed." });
}

try {
  // 执行提现逻辑(数据库原子操作+事务)
} finally {
  // 无论成功失败都释放锁
  await releaseLock(req.user._id);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 01:50:31