Spring事务遇上的灵魂拷问:bug or feature?
2023-11-06 11:32:09
事情是这样的,上周我正在全神贯注的摸鱼,然后有个小伙伴给我发来微信消息,提出了自己关于事务的一个疑问,并配上两段代码:
@Transactional
public void transfer(String fromAccountId, String toAccountId, int amount) {
Account fromAccount = accountRepository.findById(fromAccountId).get();
Account toAccount = accountRepository.findById(toAccountId).get();
fromAccount.setBalance(fromAccount.getBalance() - amount);
toAccount.setBalance(toAccount.getBalance() + amount);
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
}
这段代码实现了一个转账操作,在同一个事务中,先查询两个账户的余额,然后更新账户的余额,最后保存两个账户。
@Transactional
public void transfer(String fromAccountId, String toAccountId, int amount) {
Account fromAccount = accountRepository.findById(fromAccountId).get();
Account toAccount = accountRepository.findById(toAccountId).get();
fromAccount.setBalance(fromAccount.getBalance() - amount);
accountRepository.save(fromAccount);
toAccount.setBalance(toAccount.getBalance() + amount);
accountRepository.save(toAccount);
}
这段代码也是实现了一个转账操作,但它将两个账户的更新操作分成了两个事务。
我的小伙伴问,这两个代码在并发情况下,会不会出现问题?
我回答说,第一个代码在并发情况下可能会出现脏读、不可重复读或幻读。
我的小伙伴又问,为什么?
我解释说,因为 Spring的事务隔离级别默认是 Read Committed,在 Read Committed隔离级别下,一个事务只能看到其他事务已经提交的数据。如果两个事务同时执行第一个代码,那么就有可能出现这种情况:
- 事务 A 查询了账户 A 和账户 B 的余额。
- 事务 B 查询了账户 A 和账户 B 的余额。
- 事务 A 更新了账户 A 和账户 B 的余额。
- 事务 B 更新了账户 A 和账户 B 的余额。
事务 A 和事务 B 都认为自己已经更新了账户 A 和账户 B 的余额,但实际上,只有一个事务的更新操作成功了。
我的小伙伴恍然大悟,他说,原来是这样。
我继续说,第二个代码就不会出现这个问题,因为它是将两个账户的更新操作分成了两个事务。这样,每个事务都只更新了一个账户的余额,就不会出现并发问题。
我的小伙伴又问,那为什么 Spring的事务隔离级别默认是 Read Committed,而不是 Serializable?
我说,Serializable隔离级别可以防止脏读、不可重复读和幻读,但它也会导致性能下降。Spring官方认为,Read Committed隔离级别在大多数情况下已经足够了。
我的小伙伴点了点头,说,我明白了。
我对他说,如果你想了解更多关于 Spring 事务的知识,可以去看看 Spring 官方文档。
我的小伙伴说,好,我回去看看。
以上就是我和小伙伴关于 Spring 事务的对话。我希望这篇博文能帮助你更好地理解 Spring 事务。