MongoDB多文档事务可保证转账原子性,但必须部署在副本集或分片集群上,使用withTransaction()封装、同库集合、条件更新及同一session,禁止单节点、跨库、手动commit或事务内耗时操作。

MongoDB 多文档事务能保证转账原子性,但前提是部署在副本集或分片集群上,且所有操作必须走同一个 session、同个数据库内的集合——单节点、跨库、手动 startTransaction() 都会失效或埋雷。
必须用 withTransaction() 封装,不能手动 commit/abort
手动调用 session.startTransaction() + session.commitTransaction() 看似可控,实则极易出错:网络中断时 commitTransaction() 失败,但 abortTransaction() 没被触发,事务卡在 pending 状态,后续写操作被阻塞。官方明确推荐 session.withTransaction(),它自动处理:
- 可重试错误(如
TransientTransactionError)默认重试一次 - 回调函数内任何未捕获异常或
throw都触发abortTransaction() - 回调返回 Promise 且 resolve 后才执行
commitTransaction() - 不建议在回调里
try/catch吞掉所有错误,否则事务可能误判为成功
转账操作必须用条件更新 + 原子写锁,不能先查后改
“查 A 余额 → 查 B 余额 → A 扣款 → B 加款” 是典型并发陷阱。两个并发转账可能同时通过余额校验,最终导致超扣或少加。正确做法是用 updateOne() 的原子条件更新:
- A 扣款:只在
{ _id: fromId, balance: { $gte: amount } }条件下执行{ $inc: { balance: -amount } } - B 加款:直接
{ $inc: { balance: amount } },无需条件(假设目标账户存在且可信) - 两次
updateOne()都必须传入同一{ session }选项,否则不在事务内 - 若 A 扣款失败(匹配不到文档),整个事务自动 abort,B 不会加款
所有集合必须在同一数据库,且不能混入非 DB 操作
MongoDB 事务不支持跨数据库操作。如果账户表在 bank 库、日志表在 audit 库,即使集群相同,也会报错:Multi-document transactions do not support operations across multiple databases。实操要点:
- 把
accounts、transactions、logs全部建在同一个数据库下(比如finance) - 所有操作显式传
{ session },例如:db.collection('accounts').updateOne(..., { session }) - 禁止在
withTransaction()回调里发 HTTP 请求、写文件、调外部 SDK——这些无法回滚,破坏原子语义 - 事务默认超时 60 秒(
transactionLifetimeLimitSeconds),避免在事务里做耗时计算或等待用户输入
本地开发必须启副本集,连接字符串要带 replicaSet
用 mongod --port 27017 直连单节点,事务一定失败,报错:Transaction numbers are only allowed on a replica set member or mongos。验证和开发阶段必须:
- 启动时加
--replSet rs0参数,例如:mongod --replSet rs0 --port 27017 --dbpath /data/db - 连上后执行
rs.initiate()初始化副本集 - 应用连接字符串必须包含
?replicaSet=rs0,否则驱动无法启用事务支持 - Node.js 驱动版本不低于 3.6.0,MongoDB Server 版本不低于 4.0
真正容易被忽略的不是语法,而是部署模式和事务边界:副本集没起来、连接串缺 replicaSet、跨库写入、事务里调了第三方 API——这些错误不会立刻报语法错,而是在高并发或异常场景下悄然引发资金不一致。

















