Spring Data JPA重复读取同一值:无事务为何未获取更新?
在Spring Java Web应用中使用Spring Data JPA和PostgreSQL数据库,遇到如下问题:
通过CountDownLatch控制三个接口的执行顺序:readNameTwice首次读取实体名称 → changeName修改实体名称 → readNameTwice二次读取名称 + getSecondName读取名称。
现象:readNameTwice两次读取到的都是旧名称,getSecondName能读到新名称。未手动添加@Transactional注解,却出现了这种不符合预期的缓存/一致性问题,想了解背后的原理。
补充信息:
- 即使在
readNameTwice的changeName.await()后增加等待时间,结果仍一致; - 将
MyRepository改为基于Map的内存实现时,readNameTwice能读到不同名称,符合预期。
实体类代码
@Entity public class MyEntity { @Id private Long id; private String name; }
仓库接口代码
public interface MyRepository extends JpaRepository<MyEntity, Long> {}
服务类代码
public class MyService { private CountDownLatch changeName = new CountDownLatch(1); private CountDownLatch firstRead = new CountDownLatch(1); private MyRepository myRepository; public void readNameTwice() throws InterruptedException { String oldName = myRepository.getById(ENTITY_ID).getName(); firstRead.countDown(); changeName.await(); String newName = myRepository.getById(ENTITY_ID).getName(); System.out.println("newName same as oldName? " + newName.equals(oldName)); } public void changeName() throws InterruptedException { firstRead.await(); MyEntity myEntity = myRepository.getById(ENTITY_ID); myEntity.setName("new name"); myRepository.save(myEntity); changeName.countDown(); } public void getSecondName() throws InterruptedException { firstRead.await(); changeName.await(); MyEntity myEntity = myRepository.getById(ENTITY_ID); System.out.println("getName() = " + myEntity.getName()); } }
控制器代码
@GetMapping("/readNameTwice") public void readNameTwice() throws InterruptedException { myService.readNameTwice(); } @GetMapping("/changeName") public void changeName() throws InterruptedException { myService.changeName(); } @GetMapping("/getSecondName") public void getSecondName() throws InterruptedException { myService.getSecondName(); }
核心原因是**JPA一级缓存(EntityManager缓存)**的机制,和隐式事务有关联但并非直接由隐式事务导致:
JPA一级缓存的作用
JPA的EntityManager会维护一个一级缓存,缓存当前上下文加载的实体对象,生命周期和EntityManager绑定。在Spring Data JPA中,默认每个HTTP请求会绑定一个独立的EntityManager实例。readNameTwice方法在同一个请求内执行,整个方法都处于同一个EntityManager上下文。第一次调用getById时,实体被加载到一级缓存;第二次调用getById时,JPA直接从缓存返回对象,不会再查询数据库。getSecondName能读到新值的原因
getSecondName是独立的HTTP请求,会创建全新的EntityManager上下文,没有旧实体的缓存,会直接查询数据库,因此能拿到changeName提交后的新值。内存实现符合预期的原因
基于Map的内存Repository不属于JPA体系,没有一级缓存机制,每次getById都是直接读取Map中的最新数据,所以第二次读取能拿到修改后的结果。关于隐式事务
Spring Data JPA的仓库方法(如getById、save)默认会在事务上下文执行:如果当前没有事务,会创建新事务并在方法结束后提交。但readNameTwice的两次getById处于同一个请求的EntityManager上下文,即使每次方法的事务已提交,一级缓存依然会保留实体,不会自动刷新。
如果需要在同一个请求内读取数据库最新值,可采用以下方式:
- 手动刷新实体:注入
EntityManager,在第二次读取前调用entityManager.refresh(myRepository.getById(ENTITY_ID)),强制从数据库加载最新数据。 - 禁用查询缓存:自定义查询方法(如
findById),添加@QueryHints(value = @QueryHint(name = org.hibernate.jpa.QueryHints.HINT_CACHEABLE, value = "false"))注解禁用缓存。 - 清除一级缓存:调用
entityManager.clear(),但这会清空当前上下文所有缓存实体,需谨慎使用。
内容的提问来源于stack exchange,提问作者WZJoe

