事务回调被重复执行是 MongoDB 驱动对 TransientTransactionError 和 UnknownTransactionCommitResult 的自动重试机制所致,最多 5 次,因此回调必须幂等,避免非幂等操作,确保执行多次与一次结果一致。

事务回调被重复执行的典型现象
你看到 session.withTransaction() 里的回调函数被调用了两次甚至更多次,但只期望它运行一次。日志里反复出现相同的 insert / update 操作,或者数据库里凭空多出几条记录。这不是 Promise 重复 resolve 导致的,而是 MongoDB 驱动在特定错误下主动重试整个回调函数。
TransientTransactionError 触发自动重试
驱动默认对两类错误自动重试整个事务回调: TransientTransactionError 和 UnknownTransactionCommitResult。它们不是你代码里 throw 出来的,而是服务端返回的、表示“这次失败不一定是最终失败”的信号。
-
TransientTransactionError常见于:主从切换瞬间、网络抖动导致心跳超时、WiredTiger 内存压力大触发临时锁等待 -
UnknownTransactionCommitResult多发生在:客户端发起 commit 后网络中断,无法确认服务端是否真正提交成功 - 重试次数默认是 5 次,由驱动内部控制,你无法用 try/catch 拦截或跳过
为什么你的业务逻辑不能假设回调只执行一次
事务回调函数必须是幂等的——哪怕执行 5 次,结果也得和执行 1 次一致。否则就会出现数据重复、状态错乱等问题。
- 避免在回调里做非幂等操作:比如调用外部 HTTP 接口、发邮件、写本地文件
- 写数据库时优先用
upsert: true或唯一索引 +insertOne(捕获DuplicateKeyError后忽略) - 如果必须做副作用,把标识写进事务内集合(如
transactions_log),用findOneAndUpdate去查+记,再决定是否执行 - 别依赖闭包变量计数或全局状态判断“这是第几次”,回调每次都是全新执行上下文
如何确认是不是重试导致的重复
打开 MongoDB 日志或用 db.currentOp() 查看事务 opid 是否相同;更直接的是在回调开头加一句 console.log('TX start:', Date.now()),观察时间戳是否密集连续出现多次。
- 如果每次时间差在毫秒级且 opid 相同,基本就是重试
- 如果时间差几秒以上、opid 不同,可能是你代码里误写了多个
withTransaction()调用 - 驱动不会重试你手动抛出的
Error,只重试那两类服务端错误
事务回调的重复执行不是 bug,是设计使然。关键不在阻止它发生,而在让回调本身能扛住它。











