WriteConflict(错误码112)是WiredTiger因MVCC快照冲突主动中止事务的机制,非代码错误;应避免单文档操作套事务,优先用findOneAndUpdate+version乐观锁,跨文档一致性才启用事务,并在外层新建session、随机退避重试。

不是事务本身有问题,而是你用错了场景或没绕开 WiredTiger 的写锁机制——单文档更新硬套事务,等于主动制造冲突热点。
WriteConflict(错误码 112)只发生在多文档事务里
WiredTiger 对文档加的是物理写锁,不是逻辑锁。只要两个事务碰了同一个文档(哪怕只读一次、只改一个字段),后提交的那个就会被中止。单文档的 updateOne 或 findOneAndUpdate 天然原子,根本不需要事务包装;一旦包进 session.withTransaction(),反而触发锁竞争。
- 错误典型:查订单 → 改状态 → 更新库存,三个操作全塞进一个事务,但其实只有库存文档和订单文档是跨文档的,其他都是单点操作
- 正确拆分:库存扣减用
findOneAndUpdate+version字段做乐观锁;订单状态流转也单独原子更新;只在真正需要跨集合强一致时(比如扣库存+生成履约单)才启事务 - 注意:
TransientTransactionError是驱动抛出的封装异常,但真正判断依据必须是error.code === 112,不能只靠 errorLabels
分片集群下事务冲突更容易被放大
4.2+ 跨分片事务走两阶段提交(2PC),prepare 阶段就要所有分片响应 readConcern: "majority"。如果任一分片没开 enableMajorityReadConcern,或小版本号不一致(如 4.2.20 vs 4.2.18),prepare 响应格式错位,mongos 会静默拒绝,日志里只报 NotMasterOrSecondary 或 ReadConcernMajorityNotAvailableYet,看起来像写冲突,其实是协调失败。
- 检查每个 shard 的 primary:执行
db.runCommand({ getCmdLineOpts: 1 }),确认parsed.enableMajorityReadConcern === true - 比对所有分片的完整版本:执行
db.version()和db.runCommand({ buildInfo: 1 }),gitVersion必须 100% 相同,连-dirty后缀都不能有 - 滚动升级必须按顺序:先升 config server 副本集,再逐个升 shard,严禁跳步
重试逻辑没建在事务外层,反而让冲突更顽固
WriteConflict 必须由应用层捕获并重试,但重试位置错了就白忙——常见错误是 try/catch 里递归调用事务函数,复用旧 session,或重试前没重新 findOne 拿最新数据。
- 每次重试必须调用
client.startSession()新建 session,不能复用上一轮的 - 事务函数体里不能依赖外部 mutable 变量(比如闭包里的计数器),否则重试时行为不一致
- 退避必须带随机抖动:
await new Promise(r => setTimeout(r, Math.random() * 10)),否则高并发下所有事务同步醒来,形成“重试风暴” - 只判断
matchedCount === 0不够,还要检查lastErrorObject?.code === 11000(唯一键冲突)等其他可重试错误
真正难处理的不是 WriteConflict 本身,而是它背后暴露的数据模型问题:比如把高频更新的订单状态和低频变动的用户资料硬塞进同一文档,或者没用索引导致查询扫描扩大、无意锁住更多文档。这些细节在压力测试里不显眼,上线后才集中爆发。


















