事务必须运行在副本集上,单机部署会报错“transaction numbers are only allowed on a replica set member or mongos”;需启动时加--replset rs0、执行rs.initiate(),连接字符串带?replicaset=rs0,且每个操作显式传入session;推荐使用withtransaction()自动处理提交回滚与重试,避免手动管理导致资源泄漏或原子性失效;单文档操作无需事务,因其天然原子。

事务必须跑在副本集上,单机mongod直接报错
本地开发时只起一个 mongod --port 27017,事务一定会失败,错误是:Transaction numbers are only allowed on a replica set member or mongos。这不是驱动或代码问题,是 MongoDB 服务端硬性限制。
最轻量启动方式:
- 启动时加参数:mongod --replSet rs0 --port 27017
- 连上去执行:rs.initiate()
- 连接字符串里必须带 ?replicaSet=rs0,否则 client.startSession() 虽不报错,但后续事务操作仍会失败
session 必须显式创建并传给每个操作
常见错误是调了 client.startSession(),却忘了把 { session } 作为选项传给 insertOne()、updateOne() 等所有操作。漏传一次,那个操作就游离在事务外,原子性彻底失效。
-
session是独立对象,不是全局上下文,不能靠“作用域自动绑定” - 必须在
client.connect()之后调用client.startSession() - 同一个
session不能复用:事务结束后要调session.endSession(),再client.startSession()开新会话
优先用 withTransaction(),别手动 commit/abort
手动控制事务容易漏掉 abortTransaction(),尤其在 Promise 拒绝未捕获、异步异常抛出等场景下,会导致事务卡住、连接池耗尽,日志里出现 transaction is already committed or aborted 这类误导性错误。
withTransaction() 内部自动处理成功提交和异常回滚,还默认重试 5 次(应对临时冲突或网络抖动),更鲁棒:
await session.withTransaction(async () => {
await db.collection('orders').insertOne({ status: 'pending' }, { session });
await db.collection('inventory').updateOne({ sku: 'abc' }, { $inc: { qty: -1 } }, { session });
});
注意:
- 回调函数必须返回 Promise(哪怕只是 Promise.resolve())
- 回调里任何 throw、Promise.reject() 或未 catch 的 rejected promise 都会触发 abort
- 别在回调里再调 session.commitTransaction(),会报 Transaction already completed
别为单文档操作套事务,性能代价很大
MongoDB 单文档所有操作(包括嵌套字段更新、数组修改)天然原子,加事务纯属浪费资源。实测显示,无谓事务会使 WiredTiger Cache 被快照数据占满,吞吐量下降 40%~50%。
真正需要事务的典型场景只有:
- 跨集合操作(如账户扣款 + 转账日志写入)
- 跨数据库操作(同副本集内,且所有 DB 已存在)
- 必须保证多文档状态一致的业务逻辑(如订单创建 + 库存扣减)
能用嵌入式设计解决的,就别拆集合:比如把订单项嵌进订单文档,一个 updateOne() 就搞定,比跨集合事务快得多也稳得多。











