mongodb事务必须依赖副本集,单节点环境必然失败;条件更新需在findoneandupdate中嵌入业务校验;高并发下writeconflictexception需重试但须防过载;跨服务无法事务协调,应采用saga补偿。

事务必须依赖副本集,单节点开发环境直接失效
MongoDB 的 session.startTransaction() 在 standalone 模式下必然报错:Transaction numbers are only allowed on a replica set member or mongos。这不是配置疏漏,是硬性架构限制——哪怕你本地用 Docker 跑一个 mongo:6.0 镜像,默认就是 standalone。不加 --replSet rs0 启动参数、不执行 rs.initiate(),事务连初始化都失败。
Spring Data MongoDB 的 MongoTemplate.executeInTransaction() 也绕不开这点:它只封装了客户端调用逻辑,不替你解决服务端部署层约束。很多团队在测试环境跑通了事务代码,上线后才发现集群没开副本集,commitTransaction() 持续抛 TransientTransactionError。
findOneAndUpdate 条件写错,事务就退化成“裸更新”
事务内用 updateOne({ _id: "item_123" }, { $inc: { stock: -1 } }) 是无效的。它不校验当前库存值,只管写。哪怕库存已为 0,该操作仍会成功执行,把 stock 变成 -1——事务提交时不会拦截,因为 WiredTiger 认为“写操作本身合法”。
真正起作用的是带条件的原子操作:
inventoryCollection.findOneAndUpdate(
{ _id: "item_123", stock: { $gte: 1 } },
{ $inc: { stock: -1 } },
{ session, returnDocument: "after" }
)
关键点:
- 查询条件里必须显式包含
stock: { $gte: X },否则无法在读取瞬间完成业务逻辑判断 - 返回的
result.value为null时,说明条件不满足,得立刻throw,不能等 commit 阶段 - 别用
collection.findOne()+collection.updateOne()分两步,MongoDB 事务不给读操作加文档锁,中间窗口期会被并发请求穿透
高并发下 WriteConflictException 不是异常,而是常态
两个事务同时更新同一文档(比如同一个 SKU),WiredTiger 的 MVCC 会检测到写冲突,自动 abort 其中一个,并抛 WriteConflictException。这不是 bug,是设计使然——MongoDB 选择重试而非阻塞。
但问题在于:应用层若没做重试封装,用户请求就直接失败;若做了无限重试,又可能把 CPU 打满、拖垮整个服务。某次双十一实测中,优惠券领取事务在 2000 QPS 下平均重试 2.7 次/请求,锁等待指标 locks.Global.acquireWaitCount 小时级飙升至 8 万+。
更麻烦的是,事务默认 60 秒超时(transactionLifetimeLimitSeconds),长事务卡住不仅浪费资源,还会阻塞其他写入——因为文档锁在事务提交或 abort 前一直持有。
事务边界止于副本集,跨服务协调根本不存在
MongoDB 事务天然不支持分片集群跨片操作,更别说跨数据库、跨微服务。订单创建、库存扣减、支付回调如果分布在三个独立服务里,每个服务连自己的 MongoDB 实例,那单靠 session.commitTransaction() 连“本地一致性”都保不住——A 服务事务成功,B 服务网络超时失败,系统就进入中间态。
这时候强行套事务只会掩盖问题:你看到“库存扣减成功”,但支付没发起;或者“订单已生成”,但库存实际没动。真正的解法是 Saga 补偿:每个步骤用本地事务保证原子性,失败时触发反向操作(如“增加已扣减库存”),而不是幻想一个跨系统的全局锁。
最容易被忽略的一点:事务不是银弹,它解决的是“单副本集内多文档操作的原子性”,不是“高并发下的吞吐与稳定性”。写冲突、锁等待、超时重试、部署约束——这些才是压垮秒杀系统的真凶。











