Pessimistic lock with JPA and Hibernate

java 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

Alt Text

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;
    
    Result
  • 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

me

Pham Duc Minh

Da Nang, Vietnam