Spring Data结合MongoDB使用悲观锁失效问题排查
问题分析
你遇到的WriteConflict异常,本质是因为MongoDB的事务机制与传统关系型数据库的悲观锁行为存在差异:
- Spring Data MongoDB的
@Lock(LockModeType.PESSIMISTIC_WRITE)并非实现阻塞式的悲观锁,它只是在事务内标记需要获取文档的排他锁,但MongoDB的多文档事务基于乐观并发控制(OCC)实现。 - 当两个事务同时操作同一文档时,后提交的事务会检测到版本冲突(通过
@Version字段),直接抛出WriteConflict异常,而非等待前一个事务释放锁。
解决方案
方案一:添加重试机制(适配现有事务逻辑)
由于WriteConflict属于TransientTransactionError(可重试异常),可以通过Spring Retry实现自动重试,让失败的请求等待前一个事务完成后重新执行。
- 引入依赖
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>1.3.4</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
- 启用重试功能
在启动类添加@EnableRetry注解:
@SpringBootApplication @EnableRetry public class YourApplication { public static void main(String[] args) { SpringApplication.run(YourApplication.class, args); } }
- 修改服务类方法
@Slf4j @Service public class TempService { private final TempRepository tempRepository; public TempService(TempRepository tempRepository) { this.tempRepository = tempRepository; } @Transactional @Retryable( value = MongoCommandException.class, exceptionExpression = "#{message.contains('WriteConflict')}", maxAttempts = 5, backoff = @Backoff(delay = 1000) ) public TempEntity updateTempEntity(String id, Integer tempResource, String action) throws InterruptedException { TempEntity te = tempRepository.findById(id).orElseThrow(() -> new RuntimeException("资源不存在")); // 模拟业务耗时 TimeUnit.SECONDS.sleep(2); if ("reserve".equals(action)) { if (te.getTempResource() >= tempResource) { te.setTempResource(te.getTempResource() - tempResource); log.info("预约成功"); } else { log.info("资源不足"); } } else { te.setTempResource(te.getTempResource() + tempResource); log.info("资源释放"); } return tempRepository.save(te); } @Recover public TempEntity recover(MongoCommandException e, String id, Integer tempResource, String action) { log.error("重试次数耗尽,更新失败", e); throw new RuntimeException("资源更新失败,请稍后重试", e); } }
方案二:使用单文档原子更新(推荐)
MongoDB的单文档操作天生具备原子性,无需开启事务即可避免并发问题,这是资源预约场景下更高效、可靠的方案。
使用MongoTemplate实现原子性的资源扣减/释放:
@Slf4j @Service public class TempService { private final MongoTemplate mongoTemplate; public TempService(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } // 资源预约:原子性检查并扣减 public boolean reserveResource(String id, Integer tempResource) { Query query = new Query(Criteria.where("_id").is(id).and("tempResource").gte(tempResource)); Update update = new Update().inc("tempResource", -tempResource); UpdateResult result = mongoTemplate.updateFirst(query, update, TempEntity.class); boolean success = result.getModifiedCount() > 0; if (success) { log.info("预约成功"); } else { log.info("资源不足或资源不存在"); } return success; } // 资源释放:原子性增加 public void releaseResource(String id, Integer tempResource) { Query query = new Query(Criteria.where("_id").is(id)); Update update = new Update().inc("tempResource", tempResource); mongoTemplate.updateFirst(query, update, TempEntity.class); log.info("资源释放成功"); } }
总结
- 若坚持使用事务+悲观锁逻辑,必须通过重试机制处理
WriteConflict异常; - 单文档原子更新是MongoDB场景下的最优解,无需事务和锁,直接在数据库层面保证操作的原子性,彻底避免并发冲突。
内容的提问来源于stack exchange,提问作者user3544234
相关产品推荐
相关产品推荐

