You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Data结合MongoDB使用悲观锁失效问题排查

问题分析

你遇到的WriteConflict异常,本质是因为MongoDB的事务机制与传统关系型数据库的悲观锁行为存在差异:

  • Spring Data MongoDB的@Lock(LockModeType.PESSIMISTIC_WRITE)并非实现阻塞式的悲观锁,它只是在事务内标记需要获取文档的排他锁,但MongoDB的多文档事务基于乐观并发控制(OCC)实现。
  • 当两个事务同时操作同一文档时,后提交的事务会检测到版本冲突(通过@Version字段),直接抛出WriteConflict异常,而非等待前一个事务释放锁。

解决方案

方案一:添加重试机制(适配现有事务逻辑)

由于WriteConflict属于TransientTransactionError(可重试异常),可以通过Spring Retry实现自动重试,让失败的请求等待前一个事务完成后重新执行。

  1. 引入依赖
<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>
  1. 启用重试功能
    在启动类添加@EnableRetry注解:
@SpringBootApplication
@EnableRetry
public class YourApplication {
    public static void main(String[] args) {
        SpringApplication.run(YourApplication.class, args);
    }
}
  1. 修改服务类方法
@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 05:05:44