So, I have a very simple Spring Boot-based web application.
The database has one table, user
, with columns id
and username
and one record: (1, 'Joe')
.
I also have the following classes:
User
- an entity mapped to the table user
UserRepository
- a Spring Data JPA repository for it
UserService
+ DefaultUserService
- a service layer with CRUD methods
UserController
- a controller with two methods: get
and update
Application
- the main class (with @EnableTransactionManagement
annotation
on it)
So, what I'm trying to do is to test transaction isolation level READ_COMMITTED
. I'm sending two simultaneous requests:
- To
/update
, which updates a user, setting its name toJack
, then puts a current thread to sleep for 5s and then commits the transaction. - To
/get
, which repeatedly reads the same user for 10 times making a nap for 1s after every attempt.
The issue is that even after the transaction for (1) has been committed, (2) keeps returning an old value - Joe
. If I try to send another request to /get
after that, it returns Jack
, as expected, so the issue happens only if transaction (2) has started before transaction (1) has committed a change to the DB.
You may find some code for reference below.
Service:
@Service
public class DefaultUserService implements UserService {
... //fields & constructor
@Override
@SneakyThrows
@Transactional(isolation = Isolation.READ_COMMITTED)
public User read(Long id) {
User user = userRepository.findById(id).get();
Thread.sleep(1000);
return user;
}
@Override
@SneakyThrows
@Transactional
public User update(Long id, User update) {
log.info("Entering update method for user {}", id);
User user = read(id);
user.setUsername("Jack");
user = userRepository.save(user);
log.info("User {} updated, falling asleep for 5s", id);
Thread.sleep(5000);
return user;
}
}
Controller:
@RestController
public class UserController {
... //fields & constructor
@RequestMapping("/update")
public User update() {
User user = userService.update(1L, new User("Jack"));
log.info("UPDATED: {}", user);
return user;
}
@RequestMapping("/get")
public User get() {
User user = userService.read(1L);
for (int i = 0; i < 10; i++) {
log.info("READ: {}", user);
user = userService.read(1L);
}
return user;
}
}
Log output:
04:37:52.915 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:53.151 [nio-8781-exec-1] Entering update method for user 1
04:37:53.919 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:54.152 [nio-8781-exec-1] User 1 updated, falling asleep for 5s
04:37:54.922 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:55.926 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:56.932 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:57.937 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:58.943 [io-8781-exec-10] READ: ID: 1 :: Joe
04:37:59.222 [nio-8781-exec-1] UPDATED: ID: 1 :: Jack
04:37:59.947 [io-8781-exec-10] READ: ID: 1 :: Joe
04:38:00.950 [io-8781-exec-10] READ: ID: 1 :: Joe
04:38:01.956 [io-8781-exec-10] READ: ID: 1 :: Joe
Responses returned by controllers are also different.
For /update
it's:
{"id":1,"username":"Jack"}
While for /get
:
{"id":1,"username":"Joe"}
I'm using MySQL 5.7.18 and Spring Boot 2.1.0.
Any ideas about what I might be doing wrong/missing? Thanks in advance.