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

