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:
jeremy
's first name is set to Jeremy
./print-user
which prints out Jeremy
and goes to sleep./update-user
from another session and it changes jeremy
's first name to Bob
.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;
}
}
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:
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: 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=?
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