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

NodeJS交易控制器遇Mongoose查询重复执行错误,求解决方案

解决Mongoose Query was already executed错误并实现安全的多次更新操作

这个错误我之前开发交易系统时也碰到过,核心问题是你混用了Mongoose的await和回调函数两种异步调用方式,导致同一个查询被执行了两次,触发了Mongoose的查询已执行校验。

错误原因详解

当你调用await Customer.updateOne(...)同时又传入回调函数时,Mongoose会执行两次查询:

  1. 因为传入了回调,它会立即触发查询并调用回调函数;
  2. await关键字会尝试再次执行同一个查询对象。

第一次执行后,这个查询对象就被标记为「已执行」,第二次再执行就会抛出Query was already executed的错误。首次运行可能因为异步流程的巧合没暴露,但后续请求就会稳定触发这个问题。

另外,你的代码还有一个隐藏风险:转账的两个更新操作(转出和转入)没有原子性保证,如果其中一个操作失败,会出现数据不一致(比如钱从转出账户扣了,但没转入接收账户)。

修复方案

1. 移除回调,统一用async/await + try/catch

完全抛弃回调函数,用await处理所有异步数据库操作,配合try/catch捕获错误,保持代码风格一致。

2. 加入事务保证交易原子性

转账是典型的需要原子性的场景,必须保证两个更新操作同时成功或同时失败。我们可以用Mongoose的会话(Session)和事务来实现这一点。

修改后的完整代码

const Transaction = require('../models/transaction')
const Customer = require('../models/customers')
const mongoose = require('mongoose'); // 引入mongoose核心模块

const cashTransaction = async (req, res, next) => {
    const { from, to, amount } = req.body

    // 启动数据库会话并开启事务
    const session = await mongoose.startSession();
    session.startTransaction();

    try {
        // 绑定会话到查询,确保操作在事务内执行
        let sender = await Customer.findOne({ name: from }).session(session);
        if (!sender) {
            throw new Error('Sender customer not found');
        }

        const senderBalance = Number(sender.balance) - Number(amount);
        if (senderBalance < 0) {
            throw new Error('Insufficient balance for transaction');
        }

        // 执行转出更新,绑定会话
        await Customer.updateOne(
            { name: from },
            { balance: senderBalance }
        ).session(session);

        // 查询接收方,绑定会话
        let receiver = await Customer.findOne({ name: to }).session(session);
        if (!receiver) {
            throw new Error('Receiver customer not found');
        }

        const receiverBalance = Number(receiver.balance) + Number(amount);
        // 执行转入更新,绑定会话
        await Customer.updateOne(
            { name: to },
            { balance: receiverBalance }
        ).session(session);

        // 保存交易记录,绑定会话
        const transaction = new Transaction({ from, to, amount });
        await transaction.save({ session });

        // 所有操作成功,提交事务
        await session.commitTransaction();
        session.endSession();

        res.status(200).json({ transaction, message: 'Transaction completed successfully' });
    } catch (error) {
        // 任何步骤出错,回滚事务
        await session.abortTransaction();
        session.endSession();

        console.error('Transaction failed:', error);
        res.status(500).send(`Transaction error: ${error.message}`);
    }
}

关键注意点

  • 移除回调函数:所有Mongoose操作都只用await,避免重复执行查询。
  • 事务原子性:所有数据库操作都绑定到同一个会话,确保转账的一致性,不会出现单边失败的情况。
  • 基础校验:添加了用户存在性和余额充足性校验,提前拦截无效请求。
  • 会话管理:事务结束后一定要关闭会话,避免资源泄漏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 10:35:41