WriteConflict 是 MongoDB 快照隔离机制触发的写冲突,因事务读/写同一文档时快照被修改而中止;需捕获 code 112/133 或 TransientTransactionError 标签并重试,且每次重试前须重新读取最新文档。

WriteConflict 不是代码写错了,而是 MongoDB 快照隔离机制在告诉你:你读/写的文档快照,刚被别人改过了。
为什么并发事务一碰就冲突?
MongoDB 事务用的是 Snapshot Isolation(SI),不是 MySQL 那种行锁等待。它不阻塞,而是“乐观执行 + 提交前校验”——只要两个事务都碰了同一个文档(哪怕只是读一次再更新),后提交的那个就会被立刻中止,抛 UncategorizedMongoDbException 并带 WriteConflict 字符串(错误码实际是 112 或 133,不同驱动版本略有差异)。
常见踩坑点:
- 事务里先
findOne()拿订单状态,再updateOne()改它 → 读操作也绑定了快照,别人一动就冲突 - 多个服务或线程用相同
_id更新同一份库存、订单、账户余额 - 没显式开启事务,但用了
@Transactional+ Spring Data MongoDB → 默认进事务上下文,悄悄中招
Node.js 驱动里怎么捕获和重试?
Node.js 的 mongodb 驱动(v6+)在 commitTransaction() 失败时,不会统一抛 MongoServerError,而是分层抛错:MongoTransientTransactionError(可重试)、MongoCommitError(不可重试),或带 code: 112 的 MongoServerError。关键判断依据是:
-
error.code === 112或error.code === 133→ 写冲突,可重试 -
error.errorLabels?.includes('TransientTransactionError')→ 比看name更可靠,有些版本会把临时错误包装成MongoServerError但打上这个 label - 别依赖
error.name === 'MongoCommitError'做分支逻辑 —— 很多冲突发生在操作阶段(如updateOne),根本走不到commit
Spring Boot 里重试必须精准匹配字符串
Spring Data MongoDB 默认不带自动重试,得靠 spring-retry。但不能只捕获 UncategorizedMongoDbException 就重试,否则网络超时、权限错误、语法错误全被误重试。
正确姿势是用表达式匹配错误消息里的关键字:
@Retryable(
value = UncategorizedMongoDbException.class,
exceptionExpression = "#{message.contains('WriteConflict')}",
maxAttempts = 8,
backoff = @Backoff(delay = 100, multiplier = 2.0)
)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateOrderStatus(String orderId, String newStatus) {
// 实际逻辑
}
注意:delay = 100 是首次延迟,multiplier = 2.0 表示每次翻倍(100 → 200 → 400…),避免雪崩;maxAttempts = 8 是硬上限,再高大概率说明设计有问题,比如单文档热点没拆分。
最容易被忽略的三个实操细节
光写个重试循环不够,线上丢更新往往卡在这三点:
- 每次重试前没重新
findOne()拿最新文档 → 复用第一次读出的version和业务字段,等于拿过期快照反复撞墙 - 更新前调了发短信、扣第三方账户等外部服务,但没做幂等 → 重试就重复扣款、重复发通知
- 只判断
result.matchedCount === 0,漏了result.lastErrorObject?.code === 11000(唯一键冲突)→ 这类失败也得退避重试,不能当业务异常吞掉
Node.js 驱动还要额外注意:returnDocument 默认是 'before',你拿到的是旧值,无法用于下一轮重试;必须显式设为 'after'。











