为什么@Transactional 隔离级别在使用 Spring Data JPA 更新实体时不起作用?

esc*_*380 6 java spring spring-transactions spring-data-jpa spring-boot

对于这个基于spring-boot-starter-data-jpa依赖和H2内存数据库的实验项目,我定义了一个User具有两个字段(idfirstName)的实体,并UsersRepository通过扩展CrudRepository接口声明了一个。

现在,考虑一个简单的控制器,它提供两个端点:/print-user读取同一用户两次,间隔打印其名字,并/update-user用于在两次读取之间更改用户的名字。请注意,我特意设置了Isolation.READ_COMMITTEDlevel 并期望在第一次事务过程中,通过相同 id 检索两次的用户将具有不同的名称。但是相反,第一笔交易打印出两次相同的值。为了更清楚,这是完整的操作序列:

  1. 最初,jeremy的名字设置为Jeremy
  2. 然后我调用/print-userwhich 打印出来Jeremy并进入睡眠状态。
  3. 接下来,我/update-user从另一个会话调用,它将jeremy的名字更改为Bob.
  4. 最后,当第一个事务在睡眠后被唤醒并重新读取jeremy用户时,Jeremy即使名字已经更改为他的名字,它也会再次打印出来Bob(如果我们打开数据库控制台,它现在确实存储为Bob,不是Jeremy)。

似乎在这里设置隔离级别没有任何影响,我很好奇为什么会这样。

@RestController
@RequestMapping
public class UsersController {

    private final UsersRepository usersRepository;

    @Autowired
    public UsersController(UsersRepository usersRepository) {
        this.usersRepository = usersRepository;
    }

    @GetMapping("/print-user")
    @ResponseStatus(HttpStatus.OK)
    @Transactional (isolation = Isolation.READ_COMMITTED)
    public void printName() throws InterruptedException {
        User user1 = usersRepository.findById("jeremy"); 
        System.out.println(user1.getFirstName());
        
        // allow changing user's name from another 
        // session by calling /update-user endpoint
        Thread.sleep(5000);
        
        User user2 = usersRepository.findById("jeremy");
        System.out.println(user2.getFirstName());
    }


    @GetMapping("/update-user")
    @ResponseStatus(HttpStatus.OK)
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public User changeName() {
        User user = usersRepository.findById("jeremy"); 
        user.setFirstName("Bob");
        return user;
    }
    
}
Run Code Online (Sandbox Code Playgroud)

Gov*_*are 5

您的代码有两个问题。

usersRepository.findById("jeremy");在同一个事务中执行两次,您的第二次读取很可能是从Cache. 第二次读取记录时需要刷新缓存。我已经更新了使用的代码entityManager,请检查如何使用JpaRepository

User user1 = usersRepository.findById("jeremy"); 
Thread.sleep(5000);
entityManager.refresh(user1);    
User user2 = usersRepository.findById("jeremy");
Run Code Online (Sandbox Code Playgroud)

以下是我的测试用例的日志,请检查 SQL 查询:

  • 第一次读操作完成。线程正在等待超时。

休眠:选择 person0_.id 作为 id1_0_0_,person0_.city 作为 city2_0_0_,person0_.name 作为 name3_0_0_ from person person0_ where person0_.id=?

  • 触发对 Bob 的更新,它选择并更新记录。

休眠:选择 person0_.id 作为 id1_0_0_,person0_.city 作为 city2_0_0_,person0_.name 作为 name3_0_0_ from person person0_ where person0_.id=?

Hibernate:更新人集 city=?, name=? 哪里 id=?

  • 现在线程从睡眠中唤醒并触发第二次读取。我看不到任何触发的数据库查询,即第二次读取来自缓存。

第二个可能的问题是/update-user端点处理程序逻辑。您正在更改用户名但没有将其保留回来,仅调用 setter 方法不会更新数据库。因此,当其他端点Thread唤醒时,它会打印 Jeremy。

因此,您需要userRepository.saveAndFlush(user)在更改名称后调用。

@GetMapping("/update-user")
@ResponseStatus(HttpStatus.OK)
@Transactional(isolation = Isolation.READ_COMMITTED)
public User changeName() {
    User user = usersRepository.findById("jeremy"); 
    user.setFirstName("Bob");
    userRepository.saveAndFlush(user); // call saveAndFlush
    return user;
}
Run Code Online (Sandbox Code Playgroud)

此外,您需要检查数据库是否支持所需的隔离级别。您可以参考H2 事务隔离级别