WriteConflict(错误码112)是MongoDB MVCC快照校验失败导致的事务中止,非业务逻辑错误;它由Snapshot Isolation机制触发,当事务依赖的文档快照被其他事务修改时立即抛出UncategorizedMongoDbException并含"WriteConflict"字符串,需用Spring Retry精准匹配该关键字重试,同时拆分大事务、避免单文档热点,并合理调大maxTransactionLockRequestTimeoutMillis。

WriteConflict不是业务冲突,是MVCC快照检测失败
WriteConflict(错误码 112)不是你代码写错了,也不是文档被“锁死”了,而是 MongoDB 的 Snapshot Isolation(SI)机制在事务提交前发现:当前事务所依赖的文档快照,已被另一个已提交(或正在提交)的事务修改过。它立刻中止你这个事务,不重试、不等待,直接抛 UncategorizedMongoDbException 并带 "WriteConflict" 字符串。
这和 MySQL 的行锁阻塞完全不同——MongoDB 不会 hang 住请求等锁释放,而是乐观地执行、悲观地校验。只要两个事务碰巧改了同一文档(哪怕只是 updatedAt 字段),后提交者必败。
- 常见触发点:多个线程/服务同时
updateOne({ _id: orderId }, { $set: { status: "shipped" } })同一订单 - 容易被忽略:事务内先
findOne()读某文档,再更新它 → 读操作也绑定快照,别人一改就冲突 - 注意:即使你没显式开启事务,只要用了
@Transactional+ Spring Data MongoDB,默认就会进事务上下文
Spring Retry 必须匹配 WriteConflict 字符串判断
Spring 官方明确要求:不能只捕获 UncategorizedMongoDbException 就重试,因为该异常覆盖范围太广(网络超时、权限拒绝、语法错误都可能走这里)。必须用表达式精准匹配错误消息里的 WriteConflict 关键字,否则会把本不该重试的错误反复执行。
正确配置示例如下:
@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:首次重试延迟 100ms -
multiplier = 2.0:每次翻倍,即 100 → 200 → 400 → 800 → 1600ms… 避免雪崩式重试 -
maxAttempts = 8:总重试上限建议 ≤ 8,再高大概率是设计问题,不是临时抖动 - 务必加
Propagation.REQUIRES_NEW:保证每次重试都在干净新事务里,避免嵌套事务状态污染
重试不是万能解药,得先砍掉长事务和热点文档
如果重试后仍高频报错,说明你在拿胶带修炸药桶——根本矛盾没解决。WriteConflict 频发本质是两种场景叠加:
-
事务粒度太大:比如把 50 万条
insertMany塞进一个事务 → WiredTiger 缓存爆、快照膨胀、锁持有时间长达数分钟 → 冲突概率指数上升 -
单文档热点太集中:如库存扣减、计数器累加、订单状态机,所有并发请求都打在同一
_id上 → 冲突不可避免
此时光靠重试只会让延迟更毛刺、吞吐更低。必须同步做两件事:
- 把大事务拆成小事务:每批 ≤ 1000 条
insertMany,每个批次独立startTransaction()→ 事务生命周期压到 100ms 内 - 把单文档更新换成原子操作:
findOneAndUpdate或updateOne带条件更新,彻底绕过事务快照链;需要返回旧值?用returnDocument: "before"参数 - 对真·高频计数类场景(如 PV 统计),直接放弃单文档,改用分片计数器(
counter_shard_0,counter_shard_1…)再聚合
别漏掉 maxTransactionLockRequestTimeoutMillis 这个隐藏开关
MongoDB 默认事务锁等待超时仅 5 毫秒,比一次 Redis 网络往返还短。当副本集负载稍高、WAL 刷盘延迟、或 WiredTiger 页面换入慢一点,就直接触发 WriteConflict —— 这不是冲突,是等不及。
调大它能显著降低误报,但必须服务端统一配置(不能只改应用):
- 在线生效(推荐测试环境快速验证):
db.adminCommand({ setParameter: 1, maxTransactionLockRequestTimeoutMillis: 3000 }) - 配置文件(生产环境首选):在
/etc/mongod.cnf的setParameter下加一行maxTransactionLockRequestTimeoutMillis=3000 - 重启 mongod 或滚动重启副本集节点(分片集群需每台都设)
注意:该参数治标不治本。3000ms 是合理上限,设到 10000ms 以上反而会掩盖真实热点问题,让系统在低吞吐下“假装正常”。


















