示例说明
转账的两步扣款加款若非原子,中途失败会造成资金凭空消失或凭空增加。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');