- Optimistic locking in JPA and Hibernate
- Pessimistic locking in JPA and Hibernate
There are two ways to deal with concurrency conflicts, optimistic locking and pessimistic locking. Implicit optimistic locking has been already covered, if you didn’t have the chance to read my article on it, make sure you do it before continuing. Pessimistic locking is also a key tool to deal with conflicts. I’ll show you how to use it with Hibernate.
About pessimistic locking
Pessimistic locking is all about the concept of avoiding the conflicts from happening. This is done by employing different kind of database locks. There are two types of locks which can be differentiated, read locks for reading only the data and write locks for reading and writing the data. Multiple readers can access the same row at the same time. If someone wants to write a row and acquires a write lock, only that single writer is allowed to access it and noone else, even reading is prohibited.
With read/shared locks the database engine can guarantee that if a row is read, no other concurrent transaction can modify it thus preventing the non-repeatable read anomaly. If a write/exclusive lock is applied, then no other reader or writer can access the data.
The reality is a bit complicated though. Different database engines support different type of locks, some of them only support exclusive locks. Additionally, locks can be usually acquired with syntax differences between vendors. Hibernate can help here to wrap all these things. It can handle the syntax differences and the lock levels will be requested based on the underlying database, e.g. if the database only supports exclusive locks but you request only for a shared lock, Hibernate will automatically request an exclusive lock as that’s the only possibility for that database.
Although it sounds really great to use locks, it has one big downside. Locks will be held until the transaction ends which obtained the locks. This can be an issue especially for long-running transactions. Imagine a situation when a transaction takes 10 seconds with requesting an exclusive lock at the beginning and keeping it until the transaction ends. No other reader or writer can access the same data for 10 seconds which is unacceptable for a performance oriented application.
In the next couple of minutes, I’ll show you how to handle pessimistic locking on Hibernate level which will result in a database lock. Note that there are other explicit locks as well which I’ll cover in an upcoming article.
Using the explicit pessimistic locking mechanism in JPA/Hibernate
Unlike optimistic locking (as seen in my previous article), pessimistic locking can only be utilized by explicitly telling Hibernate which lock is needed for the operation. The idea is that when you are requesting data via Hibernate, you can tell the framework to apply a shared or exclusive lock on the data you are requesting. In the following quick example you can check how this is possible with a simple EntityManager#find
@Test public void testExclusiveLockCanBeAcquired() { // given Product p = new Product("Notebook", 5); txRunner.doInTransaction(em -> { em.persist(p); }); // when Product result = txRunner.doInTransaction(em -> { return em.find(Product.class, p.getId(), LockModeType.PESSIMISTIC_WRITE); }); // then assertThat(result).isNotNull(); }
As you can see, there is a 3rd parameter for the find method which is an instance of the LockModeType enum.
Two values are interesting for now:
- PESSIMISTIC_READ
- PESSIMISTIC_WRITE
The first one will request a shared lock from the database, the second one will be an exclusive lock.
The test now requested an exclusive lock and this can be easily seen in the generated SQL as well:
Hibernate: select product0_.id as id1_0_0_, product0_.name as name2_0_0_, product0_.stock as stock3_0_0_ from product product0_ where product0_.id=? for update
Note the FOR UPDATE clause at the end of the select statement. I’m using H2 to run this test and the syntax is depending on the database as I mentioned previously. For example SQL Server doesn’t have FOR UPDATE instead it operates with query hints to handle locking.
Read/shared locks
For showcasing read locks, the tests need to be switched to PostgreSQL as H2 only offers exclusive locks. However, I’ll just show you what happens when a shared lock is requested from such database. Same test as before with just a tiny little change to request a shared lock.
@Test public void testSharedLockCanBeAcquired() { // given Product p = new Product("Notebook", 5); txRunner.doInTransaction(em -> { em.persist(p); }); // when Product result = txRunner.doInTransaction(em -> { return em.find(Product.class, p.getId(), LockModeType.PESSIMISTIC_READ); }); // then assertThat(result).isNotNull(); }
The resulting SQL is the same as before:
Hibernate: select product0_.id as id1_0_0_, product0_.name as name2_0_0_, product0_.stock as stock3_0_0_ from product product0_ where product0_.id=? for update
As the underlying database doesn’t support shared locks, Hibernate requests an exclusive lock as it has all the guarantees as a shared lock and even more than that thus preventing any other writers from changing the data.
Switching to PostgreSQL (which supports both type of locks) and executing the same test case will result in the following SQL:
Hibernate: select product0_.id as id1_0_0_, product0_.name as name2_0_0_, product0_.stock as stock3_0_0_ from product product0_ where product0_.id=? for share
PostgreSQL supports shared locks via the FOR SHARE clause as you can see in the generated SQL. After the basics, continuing with executing some real tests demonstrating that multiple readers can access the very same row at the same time.
@Test public void testSharedLockCanBeAcquiredByMultipleReaders() { // given Product p = new Product("Notebook", 5); txRunner.doInTransaction(em -> { em.persist(p); }); // when Product result = txRunner.doInTransaction(em1 -> { Product product1 = em1.find(Product.class, p.getId(), LockModeType.PESSIMISTIC_READ); txRunner.doInTransaction(em2 -> { Product product2 = em2.find(Product.class, p.getId(), LockModeType.PESSIMISTIC_READ); txRunner.doInTransaction(em3 -> { return em3.find(Product.class, p.getId(), LockModeType.PESSIMISTIC_READ); }); return product2; }); return product1; }); // then assertThat(result).isNotNull(); }
This test simulates the situation when 3 readers try to read the same entity concurrently. As the test passes successfully and the generated SQL contains 3 select statements for the same row with the FOR SHARE clause, it’s clear that multiple readers can access the same data at the same time.
Hibernate: select product0_.id as id1_0_0_, product0_.name as name2_0_0_, product0_.stock as stock3_0_0_ from product product0_ where product0_.id=? for share Hibernate: select product0_.id as id1_0_0_, product0_.name as name2_0_0_, product0_.stock as stock3_0_0_ from product product0_ where product0_.id=? for share Hibernate: select product0_.id as id1_0_0_, product0_.name as name2_0_0_, product0_.stock as stock3_0_0_ from product product0_ where product0_.id=? for share
Write/exclusive locks
Going forward with the exclusive locks. The basic idea for these type of locks is that no other transaction can modify or read the data which is locked. The idea behind is to prevent the non-repeatable read and dirty read anomalies on the read side and the lost update anomaly on the write side.
Checking the previous example when multiple readers were present, now let’s have 1 reader who already locked a database row and 1 writer who wants to lock the same row exclusively.
@Test(expected = LockTimeoutException.class) public void testExclusiveLockCantBeAcquiredWhenSharedLockAlreadyPresent() { // given Product p = new Product("Notebook", 5); txRunner.doInTransaction(em -> { em.persist(p); }); // when Product result = txRunner.doInTransaction(em1 -> { Product product = em1.find(Product.class, p.getId(), LockModeType.PESSIMISTIC_READ); txRunner.doInTransaction(em2 -> { return em2.find(Product.class, p.getId(), LockModeType.PESSIMISTIC_WRITE, ImmutableMap.of("javax.persistence.lock.timeout", 0)); }); return product; }); // then exception thrown }
In the test, the first transaction at the beginning locks the database row in shared mode then a second parallel transaction comes into the picture and tries to lock the same row exclusively which will fail as a shared lock was already applied on the data. Note that a third parameter is provided as well when locking the row exclusively which is necessary to stop the select statement from hanging. This is how PostgreSQL will know that it shouldn’t wait for acquiring the exclusive lock, but this will be covered in more details on the Timeouts section.
In the log, there’s even a message that the exclusive lock cannot be obtained.
Hibernate: select product0_.id as id1_0_0_, product0_.name as name2_0_0_, product0_.stock as stock3_0_0_ from product product0_ where product0_.id=? for update nowait [main] o.h.engine.jdbc.spi.SqlExceptionHelper: ERROR: could not obtain lock on row in relation "product"
The same will happen when multiple writers try to lock the same row, I don’t think it’s necessary to show it as the only difference in the test is to use exclusive locks in both places.
Timeouts
There was already a timeout setting in the previous example when the lock couldn’t be obtained because of an already existing lock. Timeouts are used for avoiding deadlocks in the system. If a lock cannot be acquired after a certain amount of time, the request must be canceled and retried. Currently, there is no API available in JPA to configure lock timeouts but it can be set via query hints. It’s just basically a map of String – Object elements where different optimizations, configurations can be set for the underlying provider and database.
Lock timeout is no different, it can be set with the javax.persistence.lock.timeout query hint along with a timeout value in milliseconds.This is how it was set in the previous example:
em.find(Product.class, p.getId(), LockModeType.PESSIMISTIC_WRITE, ImmutableMap.of("javax.persistence.lock.timeout", 0));
Unfortunately, there is no standard handling lock timeouts and different vendors handle it differently. For example in PostgreSQL, setting the lock timeout to any positive value will have no effect at all, however with a value of 0, Hibernate will add the NOWAIT modifier at the end of the query which will instruct the database not to wait for the lock to be available.
I think it’s essential to understand the capabilities of the underlying database and how lock timeouts are supported as it will have an impact on the system. For this purpose always check the database documentation. On top of that, make sure you use timeouts for your pessimistic locks, don’t get into a situation which allow deadlocks.
Other ways to lock
The basic use case was covered when locking the database row along with fetching it. There are other ways to do the locking as well.
EntityManager#lock can be used when you already have a fetched entity.
@Test public void testSharedLockCanBeAcquiredAfterFetching() { // given Product p = new Product("Notebook", 5); txRunner.doInTransaction(em -> { em.persist(p); }); // when Product result = txRunner.doInTransaction(em -> { Product product = em.find(Product.class, p.getId()); em.lock(product, LockModeType.PESSIMISTIC_READ); return product; }); // then assertThat(result).isNotNull(); }
First the product is fetched and then it is getting locked resulting in executing two statements.
Hibernate: select product0_.id as id1_0_0_, product0_.name as name2_0_0_, product0_.stock as stock3_0_0_ from product product0_ where product0_.id=? Hibernate: select id from product where id =? for share
It’s also possible to lock multiple rows at the same time, this can be done by executing a custom select query and setting the lock mode.
@Test public void testSharedLockCanBeAcquiredForQuery() { // given Product p = new Product("Notebook", 5); txRunner.doInTransaction(em -> { em.persist(p); }); // when List<Product> result = txRunner.doInTransaction(em -> { return em.createQuery("FROM Product", Product.class).setLockMode(LockModeType.PESSIMISTIC_READ) .getResultList(); }); // then assertThat(result).isNotEmpty(); }
In this example, all the products are retrieved from the database and gets locked. If any other transaction wants to access the data with an exclusive lock, that operation will simply fail.
Hibernate: select product0_.id as id1_0_, product0_.name as name2_0_, product0_.stock as stock3_0_ from product product0_ for share of product0_
Summary
In this article, we’ve checked how pessimistic locks can be used with JPA and Hibernate and what will be the database level implication of them. We’ve also checked how timeouts can be applied to prevent deadlocks from happening in case of an unsuccessful lock request. As always, when it comes to deciding what type of locking pattern to apply, optimistic or pessimistic, it’s a matter of business requirement so give yourself the time to think it through which one to choose.
All the tests are available on my GitHub, check it out.
If you liked the article, make sure you share it so others can benefit from it as well and follow me on Twitter for more interesting articles.