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

Spring中READ_COMMITTED隔离级别重复读取未获已提交更新值问题

问题描述

我在Spring结合MySQL环境下测试事务隔离级别,以下是数据更新与读取方法:

//jpa repository
private final ProductRepository productRepository;

@Override
@Transactional
public ProductEntity update(Long id, String title, Long quantity) {
    Optional<ProductEntity> productOpt = productRepository.findById(id);
    if(productOpt.isPresent()) {
        ProductEntity product = productOpt.get();
        if(title != null) {
            product.setProductTitle(title);
        }
        if(quantity != null) {
            product.setQuantity(quantity);
        }
        productRepository.saveAndFlush(product);
        if(debug == 1) {
            throw new RuntimeException("Could not update product.");
        }
        return product;
    }
    throw new NoSuchElementException("Could not find product with id: " + id);
}

@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
public ProductEntity findByIdReadCommitted(Long id) {
    Optional<ProductEntity> productOpt = productRepository.findById(id);
    Optional<ProductEntity> productOpt2 = productRepository.findById(id);
    return productOpt.orElseThrow();
}

初始商品quantity值为1,测试场景:

  • 线程1启动,执行productOpt = productRepository.findById(id)读取商品,在断点暂停;
  • 线程2启动,将同一商品quantity更新为0,调用saveAndFlush()但未提交事务;
  • 线程1继续执行,读取到quantity为1,符合READ_COMMITTED隔离级别预期;
  • 线程2提交事务,数据库中quantity已变为0;
  • 线程1继续执行第二次productRepository.findById(id),仍读取到quantity为1,与预期的0不符。

疑问:为何第二次读取结果还是1?是否是缓存导致?如何验证?


核心原因:Hibernate一级缓存(Session缓存)

这是**Hibernate一级缓存(Session级缓存)**导致的。在Spring的@Transactional注解下,整个方法会共用同一个Hibernate Session:

  • 第一次调用findById时,实体被加载到Session缓存中;
  • 第二次调用findById时,Hibernate会直接从Session缓存中取数据,不会再发送SQL查询到MySQL数据库,自然读不到线程2提交的最新数据。

READ_COMMITTED隔离级别是数据库层面的规则,但Hibernate的一级缓存会在会话内优先复用已加载的实体,绕过数据库查询。


验证方法
  1. 查看SQL日志:开启Spring Data JPA的SQL日志输出,会发现第二次findById并没有执行SELECT语句。
    在application.properties中添加配置:
    spring.jpa.show-sql=true
    spring.jpa.properties.hibernate.format_sql=true
    
  2. 手动清除Session缓存:在第二次查询前清除缓存,验证是否能读到最新数据:
    修改findByIdReadCommitted方法:
    @Autowired
    private EntityManager entityManager;
    
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public ProductEntity findByIdReadCommitted(Long id) {
        Optional<ProductEntity> productOpt = productRepository.findById(id);
        // 清除Session缓存
        entityManager.clear();
        Optional<ProductEntity> productOpt2 = productRepository.findById(id);
        return productOpt.orElseThrow();
    }
    
    此时第二次查询会发送SQL到数据库,就能读到线程2提交的0值。
  3. 跨Session查询:把两次查询放到不同的事务中,比如拆分两个带@Transactional的方法分别调用,此时每个方法有独立Session,第二次查询会去数据库取最新数据。

解决方案(同一事务内读取最新数据)
  • 方法1:调用entityManager.clear()清除当前Session缓存,之后的查询会重新从数据库加载;
  • 方法2:使用entityManager.refresh(product)刷新单个实体,获取最新数据;
  • 方法3:通过原生SQL查询绕开Hibernate缓存,比如使用@Query注解执行原生SELECT语句:
    @Query(value = "SELECT * FROM product WHERE id = ?1", nativeQuery = true)
    Optional<ProductEntity> findByIdNative(Long id);
    

内容的提问来源于stack exchange,提问作者yrazlik

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 13:17:00