WriteConflict异常必须手动重试,因其是MongoDB乐观锁设计使然;Spring Data MongoDB的@Transactional不自动重试,需用@Retryable配合消息过滤精准重试,且事务须严格限定在单次HTTP请求内。

WriteConflict异常必须手动重试
MongoDB事务是乐观锁机制,WriteConflict不是偶发错误,而是设计使然——当两个事务同时修改同一文档时,后提交者会直接抛出UncategorizedMongoDbException,消息体包含"WriteConflict error"。Spring Data MongoDB的@Transactional不带重试逻辑,这点和JDBC事务完全不同。
常见错误现象:接口偶发500,日志里反复出现Command failed with error 112 (WriteConflict),但重放一次请求就成功。
- 别指望Spring Boot自动兜底,它的
MongoTransactionManager只管开启/提交/回滚,不处理冲突重试 - 官方Java Driver的
withTransaction方法内置指数退避重试,但Spring没走这条路 - 重试次数不宜设太高,
maxAttempts = 8通常够用;延迟从delay = 100起步,避免雪崩式重试压垮副本集
@Retryable注解要精准匹配异常条件
光写@Retryable(value = UncategorizedMongoDbException.class)太宽泛,可能把连接超时、权限拒绝等真错误也重试,反而掩盖问题。
必须结合exceptionExpression做消息级过滤:
- 表达式要用
#{message.contains('WriteConflict error')},注意空格和大小写,MongoDB错误消息固定带这个子串 - 不要用
getMessage().indexOf("WriteConflict") > -1这种写法,SpEL不支持链式调用方法 - 如果项目用了Logback或Log4j2,确保日志中
message字段确实透出了原始异常信息(有些全局异常处理器会吞掉)
事务边界要严格控制在单次HTTP请求内
Spring的@Transactional默认绑定到当前线程,而MongoDB事务依赖ClientSession,该对象不能跨线程或跨HTTP请求复用。
典型踩坑场景:
- 在异步方法(
@Async)里调用带@Transactional的方法 → 报No transaction in context - 事务方法里又调用另一个服务的Feign Client,对方服务也开了事务 → 嵌套事务失效,第二个事务变成独立会话,失去原子性
- WebFlux响应式流中混用阻塞式
MongoTemplate事务 → 会话上下文丢失,操作落到默认无事务会话上
真正安全的做法:所有涉及多文档一致性的操作,必须收束在一个Controller入口方法内完成,且全程不切线程、不跨服务调用。
优先用单文档原子性替代多文档事务
很多WriteConflict问题根源不在重试策略,而在建模本身。比如“扣库存+写流水”被拆成两个集合,硬靠事务保证一致性——这等于主动引入冲突热点。
更优解是重构Schema:
- 把库存流水作为嵌入数组放进
inventory文档,用updateOne配合$push和$inc一次性更新,天然单文档原子 - 用户订单和订单明细合并为一个文档,避免
orders与order_items跨集合事务 - 只有真正跨业务域、无法合并的场景(如账户余额扣减 + 积分发放),才启用多文档事务
事务不是银弹,它解决的是“不得不跨文档”的一致性,而不是“本可以不跨”的懒惰设计。


















