为什么在使用 Spring Data JPA 更新实体时 @Transactional 隔离级别无效?
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());
// 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;
}
}
您更新 @GetMapping("/update-user")
的方法设置了隔离级别 @Transactional(isolation = Isolation.READ_COMMITTED)
,因此此方法永远不会达到 commit()
步骤。
您必须更改隔离级别或读取事务中的值以提交更改:)
user.setFirstName("Bob");
不保证您的数据会被提交
线程摘要将如下所示:
A: Read => "Jeremy"
B: Write "Bob" (not committed)
A: Read => "Jeremy"
Commit B : "Bob"
// Now returning "Bob"
您的代码有两个问题。
您在同一个事务中执行了两次 usersRepository.findById("jeremy");
,您的第二次读取很可能是从 Cache
中检索记录。第二次读取记录时需要刷新缓存。我更新了使用 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); // call saveAndFlush
return user;
}
此外,您需要检查数据库是否支持所需的隔离级别。可以参考H2 Transaction Isolation Levels
对于这个基于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());
// 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;
}
}
您更新 @GetMapping("/update-user")
的方法设置了隔离级别 @Transactional(isolation = Isolation.READ_COMMITTED)
,因此此方法永远不会达到 commit()
步骤。
您必须更改隔离级别或读取事务中的值以提交更改:)
user.setFirstName("Bob");
不保证您的数据会被提交
线程摘要将如下所示:
A: Read => "Jeremy"
B: Write "Bob" (not committed)
A: Read => "Jeremy"
Commit B : "Bob"
// Now returning "Bob"
您的代码有两个问题。
您在同一个事务中执行了两次 usersRepository.findById("jeremy");
,您的第二次读取很可能是从 Cache
中检索记录。第二次读取记录时需要刷新缓存。我更新了使用 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); // call saveAndFlush
return user;
}
此外,您需要检查数据库是否支持所需的隔离级别。可以参考H2 Transaction Isolation Levels