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的一级缓存会在会话内优先复用已加载的实体,绕过数据库查询。
验证方法
- 查看SQL日志:开启Spring Data JPA的SQL日志输出,会发现第二次
findById并没有执行SELECT语句。
在application.properties中添加配置:spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true - 手动清除Session缓存:在第二次查询前清除缓存,验证是否能读到最新数据:
修改findByIdReadCommitted方法:
此时第二次查询会发送SQL到数据库,就能读到线程2提交的0值。@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(); } - 跨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
相关产品推荐
相关产品推荐

