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

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缓存)**的机制,和隐式事务有关联但并非直接由隐式事务导致:

  1. JPA一级缓存的作用
    JPA的EntityManager会维护一个一级缓存,缓存当前上下文加载的实体对象,生命周期和EntityManager绑定。在Spring Data JPA中,默认每个HTTP请求会绑定一个独立的EntityManager实例。
    readNameTwice方法在同一个请求内执行,整个方法都处于同一个EntityManager上下文。第一次调用getById时,实体被加载到一级缓存;第二次调用getById时,JPA直接从缓存返回对象,不会再查询数据库。

  2. getSecondName能读到新值的原因
    getSecondName是独立的HTTP请求,会创建全新的EntityManager上下文,没有旧实体的缓存,会直接查询数据库,因此能拿到changeName提交后的新值。

  3. 内存实现符合预期的原因
    基于Map的内存Repository不属于JPA体系,没有一级缓存机制,每次getById都是直接读取Map中的最新数据,所以第二次读取能拿到修改后的结果。

  4. 关于隐式事务
    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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 15:43:09