Pessimistic lock with JPA and Hibernate
Dec 31 2023 14:51
On the previous article, I introduce about Optimistic Lock, this technic allow you to know that data has changed by someone else
But there's pros and cons
Pros
- Easy to understand and implement
- Completely control by Java/JPA, works across multiple database
Cons
- Optimistic Lock let conflict happen
- You need to implement what to do when conflict happens (re-try, cancel...)
Let's consider this use case:
I go to social media and ask for donate to help me buy a new Car. Because I was a good guy, so all of my friend start to transfer money to my bank account
That's good, but when too many people do it, race condition may happen to my bank account's database record
In this case, Optimistic Lock will throw an exception and tell users to re-try. I don't like it, because he/she may change his mind and won't transfer anymore
So Pessimistic Lock here to help
Pessimistic Lock in hibernate is a wrapper of database's lock technic
There's 3 Pessimistic Lock mode in org.hibernate.LockMode
PESSIMISTIC_READ | allows us to obtain a shared lock and prevent the data from being updated or deleted |
PESSIMISTIC_WRITE | allows us to obtain an exclusive lock and prevent the data from being read, updated or deleted |
PESSIMISTIC_FORCE_INCREMENT | works like PESSIMISTIC_WRITE, and it additionally increments a version attribute of a versioned entity |
Code Example
package com.vominh.example.jpa;
import com.vominh.example.jpa.entity.DeviceWithVersionEntity;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CompletableFuture;
public class TestPessimisticLock {
private static final Logger log = LoggerFactory.getLogger(TestPessimisticLock.class);
public static void main(String[] args) throws InterruptedException {
DataUtils.setup();
SessionFactory sessionFactory = AppSessionFactory.buildSessionFactory();
CompletableFuture.runAsync(() -> {
log.info("Thread 1, load and lock record");
var session1 = sessionFactory.openSession();
var lockOptions = new LockOptions(LockMode.PESSIMISTIC_READ);
// Search and lock record, prevent update
session1.getTransaction().begin();
DeviceWithVersionEntity d1 = (DeviceWithVersionEntity) session1.load(DeviceWithVersionEntity.class, 1, lockOptions);
try {
// Delay close session1 to make session2 wait to commit
Thread.sleep(10000);
session1.getTransaction().commit();
session1.close();
log.info("Session 1 closed");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("Thread 1, finished");
});
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("Thread 2, load and update record");
// Try to update locking record
var session2 = sessionFactory.openSession();
DeviceWithVersionEntity d2 = (DeviceWithVersionEntity) session2.load(DeviceWithVersionEntity.class, 1);
log.info("LockMode.PESSIMISTIC_READ still allow another session to read the record, devide's name: {}", d2.getName());
try {
d2.setName("new name");
session2.getTransaction().begin();
session2.update(d2);
session2.getTransaction().commit(); // wait if session1 not close
session2.close();
log.info("Session 2 closed");
} catch (Exception e) {
log.info(e.getMessage());
e.printStackTrace();
}
log.info("Thread 2, finished");
});
// Wait for thread 1,2 finish to view log
Thread.sleep(20000);
sessionFactory.close();
log.info("EXIT");
}
}
Log result
INFO com.vominh.example.jpa.TestPessimisticLock - Thread 1, load and lock record
INFO com.vominh.example.jpa.TestPessimisticLock - Thread 2, load and update record
INFO com.vominh.example.jpa.TestPessimisticLock - LockMode.PESSIMISTIC_READ still allow another session to read the record, devide's name: Dell xps 88
INFO com.vominh.example.jpa.TestPessimisticLock - Session 1 closed
INFO com.vominh.example.jpa.TestPessimisticLock - Thread 1, finished
INFO com.vominh.example.jpa.TestPessimisticLock - Session 2 closed
INFO com.vominh.example.jpa.TestPessimisticLock - Thread 2, finished
Scenario explain
Create two thread, thread 1 load a record and lock it with LockMode.PESSIMISTIC_READ then pause thread for 10s to keep the session opening (I want it available to read, but not allow to update/delete)
When Thread 2 load the locked record, it successfully read the information
But when update it, session 2 got block cause session 1 still open, the lock is not release yet
After 10s pause, session 1 closed then session 2 is able to commit the transaction
Apply this technic to my above use case, I can receive all transfer donate money without block any transaction
Conclusion
- Pessimistic locking aims to avoid conflicts by using locking.
- Pessimistic Lock is relay on the Database, and not all database use the same lock technic. In my example, I use postgres, when executing this sql in thread 1 pause time, I can see the query that gets locked in database
select pid, usename, pg_blocking_pids(pid) as blocked_by, query as blocked_query from pg_stat_activity where cardinality(pg_blocking_pids(pid)) > 0;
- Hibernate support add timeout to LockOptions by setTimeOut(int timeout) method
- Hibernate support lock scope PessimisticLockScope.EXTENDED to block related entities in a join table
Full example project available at: Github
Delete comment
Confirm delete comment
Pham Duc Minh
Da Nang, Vietnam