We have a business application which uses Spring Boot, Hibernate, and an underlying Oracle Database (H2 database for tests).
Users make a lot of simultaneous requests to it.
To cope with the many requests we are using optimistic locking by using a @Version
field on our entities, simply retrying an operation on optimistic lock exceptions (with Spring Retry).
When either updating attributes of the same entity or using entityManager.lock(entity, OPTIMISTIC_FORCE_INCREMENT)
in parallel, deadlocks are detected.
What we would expect here would either be a javax.persistence.OptimisticLockException
or a org.springframework.orm.ObjectOptimisticLockingFailureException
. After all, deadlocks should only be caused by pessimistic locks. And the problem with deadlocks in Oracle DB is that they are only detected after a timeout, sometimes.
For reproducibility we have included an example which uses entityManager.lock(entity, OPTIMISTIC_FORCE_INCREMENT)
.
You can find the example here: https://github.com/ccschneidr/hibernate-deadlock-example/
The major parts of the example are
- an Entity called
ApplicationUser
with anint
attribute, annotated with@Version
- a test which executes two version increments in different order, simultaneously, to simulate more complex operations where the order of modifications cannot be controlled easily:
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DeadlockExamplesApplicationTests {
@Autowired
UserRepository userRepository;
@Autowired
Persistence persistence;
@Autowired
EntityManager entityManager;
@Test
public void testDeadlock() throws Exception {
Integer userId1 = persistence.transactional(() ->
userRepository.save(new ApplicationUser()).getUserid());
Integer userId2 = persistence.transactional(() ->
userRepository.save(new ApplicationUser()).getUserid());
Thread thread1 = new Thread(() -> {
try {
persistence.transactional(() -> {
ApplicationUser applicationUser1 = userRepository.findById(userId1)
.orElseThrow(() -> new IllegalStateException("Previously created user not found"));
ApplicationUser applicationUser2 = userRepository.findById(userId2)
.orElseThrow(() -> new IllegalStateException("Previously created user not found"));
entityManager.lock(applicationUser1, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
entityManager.lock(applicationUser2, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
return null;
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}, "thread1");
Thread thread2 = new Thread(() -> {
try {
persistence.transactional(() -> {
ApplicationUser applicationUser1 = userRepository.findById(userId1)
.orElseThrow(() -> new IllegalStateException("Previously created user not found"));
ApplicationUser applicationUser2 = userRepository.findById(userId2)
.orElseThrow(() -> new IllegalStateException("Previously created user not found"));
entityManager.lock(applicationUser2, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
entityManager.lock(applicationUser1, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
return null;
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}, "thread2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
As far as we saw in the SQL statements Hibernate does not explicitly lock tables/row, but the underlying database (H2 in the example, Oracle in real life) does. But this renders the ‘optimistic’ locks non-functional in our case.
The question is: How can we avoid deadlocks and completely rely on Hibernate’s optimistic locking concept with the version number?