为什么使用@Spring Data JPA更新实体时,@Transactional隔离级别无效?

huangapple go评论91阅读模式
英文:

Why does @Transactional isolation level have no effect when updating entities with Spring Data JPA?

问题

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

现在,考虑一个简单的控制器,它提供了两个端点:/print-user 读取同一个用户两次,之间有一定的时间间隔,并打印出其名字;/update-user 用于在这两次读取之间更改用户的名字。请注意,我特意设置了 Isolation.READ_COMMITTED 隔离级别,并期望在第一个事务过程中,通过相同的 id 检索到的用户会有不同的名字。但是实际上,第一个事务会两次打印出相同的值。为了更清楚,以下是完整的操作序列:

  1. 最初,jeremy 的名字被设置为 Jeremy
  2. 然后我调用 /print-user,它打印出 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());
        
        // 允许从另一个会话更改用户的名字
        // 通过调用 /update-user 端点
        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;
    }
}
英文:

For this experimental project based on the spring-boot-starter-data-jpa dependency and H2 in-memory database, I defined a User entity with two fields (id and firstName) and declared a UsersRepository by extending the CrudRepository interface.

Now, consider a simple controller which provides two endpoints: /print-user reads the same user twice with some interval printing out its first name, and /update-user is used to change the user's first name in between those two reads. Notice that I deliberately set Isolation.READ_COMMITTED level and expected that during the course of the first transaction, a user which is retrieved twice by the same id will have different names. But instead, the first transaction prints out the same value twice. To make it more clear, this is the complete sequence of actions:

  1. Initially, jeremy's first name is set to Jeremy.
  2. Then I call /print-user which prints out Jeremy and goes to sleep.
  3. Next, I call /update-user from another session and it changes jeremy's first name to Bob.
  4. Finally, when the first transaction gets awakened after sleep and re-reads the jeremy user, it prints out Jeremy again as his first name even though the first name has already been changed to Bob (and if we open the database console, it's now indeed stored as Bob, not Jeremy).

It seems like setting isolation level has no effect here and I'm curious why this is so.

@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;
    }
	
}

答案1

得分: 6

你的代码存在两个问题。

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

User user1 = usersRepository.findById("jeremy"); 
Thread.sleep(5000);
entityManager.refresh(user1);    
User user2 = usersRepository.findById("jeremy");

这是来自我的测试用例的日志,请检查 SQL 查询:

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

> Hibernate: select person0_.id as id1_0_0_, person0_.city as city2_0_0_, person0_.name as name3_0_0_ from person person0_ where person0_.id=?

  • 触发对 Bob 的更新,它先选择然后更新记录。

> Hibernate: select person0_.id as id1_0_0_, person0_.city as city2_0_0_, person0_.name as name3_0_0_ from person person0_ where person0_.id=?

> Hibernate: update person set city=?, name=? where 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); // 调用 saveAndFlush
    return user;
}

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

英文:

There are two issues with your code.

You are performing usersRepository.findById("jeremy"); twice in the same transaction, chances are your second read is retrieving the record from the Cache. You need to refresh the cache when you read the record for the second time. I have updated code which uses entityManager, please check how it can be done using the JpaRepository

User user1 = usersRepository.findById("jeremy"); 
Thread.sleep(5000);
entityManager.refresh(user1);    
User user2 = usersRepository.findById("jeremy");

Here are the logs from my test case, please check SQL queries:

  • The first read operation is completed. Thread is waiting for the timeout.

> Hibernate: select person0_.id as id1_0_0_, person0_.city as city2_0_0_, person0_.name as name3_0_0_ from person person0_ where person0_.id=?

  • Triggered update to Bob, it selects and then updates the record.

>Hibernate: select person0_.id as id1_0_0_, person0_.city as city2_0_0_, person0_.name as name3_0_0_ from person person0_ where person0_.id=?

>Hibernate: update person set city=?, name=? where id=?

  • Now thread wakes up from Sleep and triggers the second read. I could not see any DB query triggered i.e the second read is coming from the cache.

The second possible issue is with /update-user endpoint handler logic. You are changing the name of the user but not persisting it back, merely calling the setter method won't update the database. Hence when other endpoint's Thread wakes up it prints Jeremy.

Thus you need to call userRepository.saveAndFlush(user) after changing the name.

@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;
}

Also, you need to check whether the database supports the required isolation level. You can refer H2 Transaction Isolation Levels

答案2

得分: 2

你的更新@GetMapping("/update-user")方法设置了隔离级别@Transactional(isolation = Isolation.READ_COMMITTED),因此在该方法中永远不会达到commit()步骤。

你必须更改隔离级别或在事务中读取你的值以提交更改 为什么使用@Spring Data JPA更新实体时,@Transactional隔离级别无效?
user.setFirstName("Bob");不能保证你的数据会被提交。

线程摘要将如下所示:

A: 读取 => "Jeremy"
B: 写入 "Bob"未提交
A: 读取 => "Jeremy"
提交 B "Bob"

// 现在返回 "Bob"
英文:

Your method to update @GetMapping("/update-user") is set with an isolation level @Transactional(isolation = Isolation.READ_COMMITTED) so commit() step is never reached in this method.

You must change isolation level or read your value in your transaction to commit the changes 为什么使用@Spring Data JPA更新实体时,@Transactional隔离级别无效?
user.setFirstName("Bob"); does not ensure your data will be committed

Thread Summary will look like this :

A: Read => "Jeremy"
B: Write "Bob" (not committed)
A: Read => "Jeremy"
Commit B : "Bob"

// Now returning "Bob"

</details>



huangapple
  • 本文由 发表于 2020年9月15日 20:11:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/63901579.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定