英文:
Why does @Transactional isolation level have no effect when updating entities with Spring Data JPA?
问题
基于 spring-boot-starter-data-jpa
依赖和 H2
内存数据库的这个实验性项目中,我定义了一个名为 User
的实体,拥有两个字段(id
和 firstName
),并且通过扩展 CrudRepository
接口声明了一个 UsersRepository
。
现在,考虑一个简单的控制器,它提供了两个端点:/print-user
读取同一个用户两次,之间有一定的时间间隔,并打印出其名字;/update-user
用于在这两次读取之间更改用户的名字。请注意,我特意设置了 Isolation.READ_COMMITTED
隔离级别,并期望在第一个事务过程中,通过相同的 id 检索到的用户会有不同的名字。但是实际上,第一个事务会两次打印出相同的值。为了更清楚,以下是完整的操作序列:
- 最初,
jeremy
的名字被设置为Jeremy
。 - 然后我调用
/print-user
,它打印出Jeremy
并进入休眠状态。 - 接下来,我从另一个会话调用
/update-user
,将jeremy
的名字更改为Bob
。 - 最后,在第一个事务经过休眠后被唤醒并重新读取
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:
- Initially,
jeremy
's first name is set toJeremy
. - Then I call
/print-user
which prints outJeremy
and goes to sleep. - Next, I call
/update-user
from another session and it changesjeremy
's first name toBob
. - Finally, when the first transaction gets awakened after sleep and re-reads the
jeremy
user, it prints outJeremy
again as his first name even though the first name has already been changed toBob
(and if we open the database console, it's now indeed stored asBob
, notJeremy
).
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()
步骤。
你必须更改隔离级别或在事务中读取你的值以提交更改
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
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>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论