Pessimistic Locking with Hibernate
This article expands the topic of database related concurrency control in java using the Hibernate Framework. You’ll understand what Pessimistic Locking is and how to implement it with Hibernate. We’ll also explore the difference between Pessimistic Concurrency Control and previously described Optimistic option. All that comes with easy to digest examples.
Take a look at this article’s Github where we apply LockModeType settings to queries.
Why do we need concurrency control
Majority of our applications involve pulling data from the database, modifying it in some way and saving it again. Database transactions allow us to treat this sequence of operations as atomic, consistent, isolated and durable. These are so-called transaction ACID guarantees. ACID means that we want:
- our unit of work to be executed as a single command, either all commands execute or none
- the data in the database to always be in a valid state
- units of work executed just as they were running serially rather than parallel
- changes to remain in the database once a transaction is committed even if the system is down later
The important bit is that applications inherit transaction isolation rules from the database they’re using. Usually, the default transaction isolation databases offer are not enough to eliminate all the problems related to transactions running concurrently.
- Transaction isolation levels
- Transaction isolation issues
Both Optimistic and Pessimistic locking help us introduce this additional level of security we may need to make sure the data we modify inside a transaction is not modified by another transaction at the same time.
Pessimistic locking in hibernate
With JPA’s Pessimistic Locking we’re moving transaction conflict discovery as early in the cycle as possible. PessimisticLockException will be thrown when we query for rows which are already locked. If the initial select query is successful, rows which meet the select query criteria are locked for the duration of a transaction. We can be sure that no other transaction will modify them.
In order to present how to use LockModeType to lock rows pessimistically with JPA, I devised a simple test. This test method runs concurrently in three threads.
@Test
@com.anarsoft.vmlens.concurrent.junit.ThreadCount(3)
public void whenExecutingQuery_thenRowsLockedMultithreaded() {
persistRandomCars(10);
carDao.executeInTransaction(entityManager -> {
List cars = entityManager.createQuery("select c from Car c", Car.class)
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.getResultList();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("there are " + cars.size() + " cars");
});
}
There’s no change in entity structure required to lock database rows. To prevent other transactions for accessing particular rows or table we either set LockModeType on a query or when calling EntityManager.find method. @NamedQuery and EntityManager.refersh method contain similar options to define LockModeType.
H2 database locks its rows by modifying the query slightly. It will look as follows:
select car0_.id as id1_0_, car0_.make as make2_0_, car0_.mileage as mileage3_0_, car0_.model as
model4_0_ from Car car0_ for update
With LockModeType.PESSIMISTIC_WRITE, we’re preventing other transactions to read from the table, therefore two transactions in above test will throw the following exception:
javax.persistence.PessimisticLockException: could not extract ResultSet
...
Caused by: org.h2.jdbc.JdbcSQLException: Timeout trying to lock table ; SQL statement:
Difference between jpa pessimistic and optimistic locking.
With Pessimistic Concurrency Control, a shared resource is locked from the moment it is first retrieved, to the moment the transaction is committed. No other transactions will be able to access the resource at that time. Our transaction is effectively “reserving” a particular data set until it is done.
With Optimistic Locking, we’re not actually reserving anything, but rather saving the version of a particular resource at the moment we retrieve it. If at the moment of committing a transaction, the state of the resource changed, Hibernate will notice that and throw an exception. This is probably a moment when you should restart the whole transaction. With Optimistic Locking, we’re expecting transaction conflicts to be rather infrequent and we don’t lock anything in the database, but we pay a higher price when the conflict occurs.
Lock mode types in JPA specification
There are three Pessimistic LockModeTypes available in JPA specification.
LockModeType | Description |
---|---|
PESSIMISTIC_READ | Rows are locked and can be read by other transactions, but they cannot be deleted or modified. PESSIMISTIC_READ guarantees repeatable reads. |
PESSIMISTIC_WRITE | Rows are locked and cannot be read, modified or deleted by other transactions. For PESSIMISTIC_WRITE no phantom reads can occur and access to data must be serialized. |
PESSIMISTIC_FORCE_INCREMENT | Rows are locked and cannot be modified or deleted. For versioned entities, their version number is incremented as soon as the query executes. |
It’s important to note that when locking data pessimistically, only the data related to the particular table rows are locked (or whole table in case of PESSIMISTIC_WRITE). When a foreign key appears in the locked table, this relationship will be locked. When the foreign key is in another table though, the relationship will not be locked. Take a look at below diagram
When we lock a row in the Car table, its relationship with battery is preserved as we cannot change the battery_id foreign key.
On the other hand, we can freely change the relation between Car and Car_Part even though a particular Car is locked.
To compare with Optimistic Locking and review the code behind this article on Github, see the links below
- Full code reference behind this article on Github
- JPA Optimistic Locking
- What is a transaction and how can transactions impact each other
- Transaction isolation levels with examples.
- Default Hibernate transaction isolation
- Complete Spring and Hibernate application example