writeconflict(错误码112)是wiredtiger主动中止事务的协作机制,需应用层捕获并重试;典型场景为高并发更新同一批文档、读-改-写未用原子操作等;正确做法是外层while循环+新建session+随机退避,优先通过单文档原子操作和索引优化规避冲突。

WriteConflict 不是 bug,是 WiredTiger 引擎主动中止事务的协作机制;它必须由应用层捕获错误码 112 并重试,不能依赖驱动自动处理。
为什么事务里总报 WriteConflict(错误码 112)
这不是你的代码写错了,而是多个事务同时修改同一份数据时,WiredTiger 在提交阶段检测到版本冲突,主动回滚其中一个事务。典型场景包括:
- 高并发更新同一批
orderId文档(比如库存扣减、订单状态流转) - 事务内含
findOneAndUpdate或带条件的updateOne,且查询条件覆盖范围大 - 读-改-写逻辑未用原子操作,而是拆成
findOne+updateOne两步(即使在事务内,中间仍可能被其他事务抢先修改)
关键点:MongoDB 不会自动重试,也不抛出传统异常类型——它让事务以 MongoError 形式退出,error.code === 112 是唯一可靠识别依据。
如何正确捕获并重试 WriteConflict
重试必须在事务外层做,每次重试都要新建 session,且整个事务逻辑要可重入。常见错误是复用旧 session 或在事务外提前改了变量值。
- 用
while循环包裹session.withTransaction(),不是靠try/catch里递归调用 - 重试次数建议设为
3~5次,超过说明模型或锁粒度有问题(比如不该用事务更新单文档) - 每次重试前加随机退避:
await new Promise(r => setTimeout(r, Math.random() * 10)),避免雪崩 - 必须每次调用
client.startSession()新建 session,不能复用上一轮的session - 事务函数体里不能依赖外部 mutable 变量(如闭包里递增的计数器),否则重试时行为不一致
比重试更有效的预防手段
WriteConflict 高发,本质是设计没绕开冲突热点。优先考虑这些替代方案:
- 单文档更新场景,直接用
findOneAndUpdate+version字段实现乐观锁,**不要包装事务**——事务对单文档反而加重锁竞争 -
version字段必须初始化为NumberInt(1)或NumberLong(1),不能是null、undefined或字符串 - 跨文档操作才用事务,且尽量缩小事务内操作范围:比如把批量更新拆成小批次,每批用独立事务
- 用
$setOnInsert替代事务内upsert: true(后者直接报错),避免先findOne再判断的竞态窗口 - 检查索引是否覆盖查询条件——缺失索引会导致扫描扩大,无意中锁住更多文档
容易被忽略的底层影响
WriteConflict 看似只是重试问题,但它背后牵动 WiredTiger 缓存和 oplog 压力:
- 频繁冲突会堆积未提交的快照数据,挤占
wiredTigerCache,触发 eviction 风暴,拖慢整体吞吐 - 事务超时(默认 60 秒)未提交也会被强制中止,并同样报
WriteConflict,需检查是否有外部 HTTP 调用等阻塞操作 - 副本集部署下,
transactionLifetimeLimitSeconds必须在所有节点统一配置,否则部分节点提前中止事务导致不一致
真正难处理的不是怎么重试,而是怎么让重试几乎不发生——这取决于你把数据放在哪儿、用什么操作、以及是否真的需要事务。











