Mongoose事务不回滚的根本原因是未显式调用session.startTransaction()且操作未传{session},或withTransaction()回调中错误被try/catch吞没导致静默提交。

事务不回滚,基本是因为没真正“进事务”或错误没被正确抛出。 Mongoose 的事务不是自动开启的魔法开关,它依赖显式 session 绑定和精确的错误传播机制。下面直击几个高频失效点。
session.startTransaction() 必须手动调用
只调用 startSession() 不等于开启了事务——这只是创建了一个会话容器。不调用 session.startTransaction(),后续所有带 { session } 的操作都只是“带凭证的普通操作”,MongoDB 不会把它们聚合成一个原子单元。
- 必须在
withTransaction()回调外、或自定义事务流程中显式执行session.startTransaction() - 如果提前调用了
session.endSession()或 session 被 GC(比如函数返回后未保持引用),再传{ session }也会报Cannot use a session that has ended - Mongoose v6.8+ 支持多库事务,但前提是所有 Model 都基于同一
mongoose.connection,不能混用不同mongoose.createConnection()实例
withTransaction() 回调里 throw 才算触发回滚
withTransaction() 不会帮你捕获 try/catch 里的错误。它只监听 Promise rejection 或同步 throw;一旦你在回调里用 catch 吞掉异常,事务就静默提交了。
- 所有数据库操作(
save()、updateOne()、findOneAndUpdate()等)必须放在withTransaction()的回调函数内 - 校验失败时直接
throw new Error('库存不足'),不要return { success: false } - 避免在回调里做非 DB 操作:HTTP 请求、
fs.readFile、复杂计算等——这些失败不会触发回滚,还可能拖慢事务锁持有时间
findOneAndUpdate() 在事务中必须加 returnDocument 和 session
这个方法在事务中最容易“看似成功实则失效”:文档没更新、返回 null、甚至报 Cannot use a session that has ended。根本原因是它的原子性在事务上下文里被打破。
- 必须显式传
{ session, returnDocument: 'after' },缺一不可 -
returnDocument: 'after'不仅确保返回最新值,还能规避因两次 RPC 导致的 session 上下文丢失 - 不要用
new: true(那是旧版 mongoose 的写法,v6+ 已废弃,且不保证事务安全) - 如果更新条件涉及其他集合的关联数据,别硬塞进同一个事务——考虑用最终一致性 + 补偿事务替代
最易被忽略的一点:事务不是万能锁。它只保证单 replica set 内的原子性,跨分片、跨集群、混合 MongoDB 与外部系统(如 Redis、MySQL)的操作,无法靠 withTransaction() 保障一致性。这时候得靠业务层设计兜底,而不是反复调试 session 参数。


















