← 中文资源

简体中文版本

Mongoose MongoDB 事务会话

把多文档写入合并为原子事务:会话贯穿全部读写,余额不足即中止回滚,瞬时错误由 withTransaction 自动重试。

阅读英文原文

示例说明

转账的两步扣款加款若非原子,中途失败会造成资金凭空消失或凭空增加。MongoDB 4.0 起支持多文档事务,mongoose 的 withTransaction 把提交、回滚与瞬时错误重试封装成一体:回调内任何抛错都会中止整个事务,两处余额修改要么全部生效要么全部无效。

技术要点

  • session 必须传给事务内的每个读写操作
  • 余额校验失败抛错即触发整体回滚
  • writeConcern majority 保证多数节点确认

代码解读

startSession 开启会话后,withTransaction 接收业务回调:两次 findById 都带 session 选项,确保读到事务内的一致快照;save 也传 session 使写入纳入同一事务。readConcern snapshot 隔离并发修改,endSession 在 finally 里必然释放会话资源。

使用建议

事务需要副本集环境,单机模式会直接报错。事务时间窗要尽量短,长事务持有锁与快照开销大。高频扣减场景可考虑用乐观并发版本号替代事务,吞吐更高。

代码示例

import mongoose from 'mongoose';

const Account = mongoose.model('Account', new mongoose.Schema({
  holder: String, balance: { type: Number, min: 0 },
}));

async function transfer(fromId, toId, amount) {
  const session = await mongoose.startSession();
  try {
    await session.withTransaction(async () => {
      const from = await Account.findById(fromId).session(session);
      const to = await Account.findById(toId).session(session);

      if (from.balance < amount) throw new Error('insufficient funds');

      from.balance -= amount;
      to.balance += amount;
      await from.save({ session });
      await to.save({ session });
    }, { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } });
  } finally {
    await session.endSession();
  }
}

await mongoose.connect(process.env.MONGO_URI);
const receipt = await transfer('665f0a', '665f0b', 75);
console.log('transfer done:', receipt ?? 'committed');