How does LockModeType.OPTIMISTIC work in JPA and Hibernate

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Explicit optimistic locking

In my previous post, I introduced the basic concepts of Java Persistence locking.

The implicit locking mechanism prevents lost updates and it’s suitable for entities that we can actively modify. While implicit optimistic locking is a widespread technique, few happen to understand the inner workings of explicit optimistic lock mode.

Explicit optimistic locking may prevent data integrity anomalies when the locked entities are always modified by some external mechanism.

The product ordering use case

Let’s say we have the following domain model:

Product OrderLine Optimistic Lock Mode

Our user, Alice, wants to order a product. The purchase goes through the following steps:

Implicit Locking LockMode None

  • Alice loads a Product entity
  • Because the price is convenient, she decides to order the Product
  • the price Engine batch job changes the Product price (taking into consideration currency changes, tax changes and marketing campaigns)
  • Alice issues the Order without noticing the price change

Implicit locking shortcomings

First, we are going to test if the implicit locking mechanism can prevent such anomalies. Our test case looks like this:

doInTransaction(session -> {
	final Product product = (Product) session.get(Product.class, 1L);
	try {
		executeSync(() -> doInTransaction(_session -> {
			Product _product = (Product) _session.get(Product.class, 1L);
			assertNotSame(product, _product);
			_product.setPrice(BigDecimal.valueOf(14.49));
		}));
	} catch (Exception e) {
		fail(e.getMessage());
	}
	OrderLine orderLine = new OrderLine(product);
	session.persist(orderLine);
});

The test generates the following output:

#Alice selects a Product
Query:{[select abstractlo0_.id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_.id=?][1]} 

#The price engine selects the Product as well
Query:{[select abstractlo0_.id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_.id=?][1]} 
#The price engine changes the Product price
Query:{[update product set description=?, price=?, version=? where id=? and version=?][USB Flash Drive,14.49,1,1,0]}
#The price engine transaction is committed
DEBUG [pool-2-thread-1]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection

#Alice inserts an OrderLine without realizing the Product price change
Query:{[insert into order_line (id, product_id, unitPrice, version) values (default, ?, ?, ?)][1,12.99,0]}
#Alice transaction is committed unaware of the Product state change
DEBUG [main]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection

The implicit optimistic locking mechanism cannot detect external changes unless the entities are also changed by the current Persistence Context. To protect against issuing an Order for a stale Product state, we need to apply an explicit lock on the Product entity.

Explicit locking to the rescue

The Java Persistence LockModeType.OPTIMISTIC is a suitable candidate for such scenarios, so we are going to put it to a test.

Hibernate comes with a LockModeConverter utility, that’s able to map any Java Persistence LockModeType to its associated Hibernate LockMode.

For simplicity sake, we are going to use the Hibernate specific LockMode.OPTIMISTIC, which is effectively identical to its Java persistence counterpart.

According to Hibernate documentation, the explicit OPTIMISTIC Lock Mode will:

assume that transaction(s) will not experience contention for entities. The entity version will be verified near the transaction end.

I will adjust our test case to use explicit OPTIMISTIC locking instead:

try {
    doInTransaction(session -> {
        final Product product = 
            (Product) session.get(Product.class, 1L, new LockOptions(LockMode.OPTIMISTIC));

        executeSync(() -> {
            doInTransaction(_session -> {
                Product _product = (Product) _session.get(Product.class, 1L);
                assertNotSame(product, _product);
                _product.setPrice(BigDecimal.valueOf(14.49));
            });
        });

        OrderLine orderLine = new OrderLine(product);
        session.persist(orderLine);
    });
    fail("It should have thrown OptimisticEntityLockException!");
} catch (OptimisticEntityLockException expected) {
    LOGGER.info("Failure: ", expected);
}

The new test version generates the following output:

#Alice selects a Product
Query:{[select abstractlo0_.id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_.id=?][1]} 

#The price engine selects the Product as well
Query:{[select abstractlo0_.id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_.id=?][1]} 
#The price engine changes the Product price
Query:{[update product set description=?, price=?, version=? where id=? and version=?][USB Flash Drive,14.49,1,1,0]} 
#The price engine transaction is committed
DEBUG [pool-1-thread-1]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection

#Alice inserts an OrderLine
Query:{[insert into order_line (id, product_id, unitPrice, version) values (default, ?, ?, ?)][1,12.99,0]} 
#Alice transaction verifies the Product version
Query:{[select version from product where id =?][1]} 
#Alice transaction is rolled back due to Product version mismatch
INFO  [main]: c.v.h.m.l.c.LockModeOptimisticTest - Failure: 
org.hibernate.OptimisticLockException: Newer version [1] of entity [[com.vladmihalcea.hibernate.masterclass.laboratory.concurrency.AbstractLockModeOptimisticTest$Product#1]] found in database

The operation flow goes like this:

Explicit Locking LockMode Optimistic

The Product version is checked towards the transaction end. Any version mismatch triggers an exception and a transaction rollback.

Race condition risk

Unfortunately, the application-level version check and the transaction commit are not an atomic operation. The check happens in EntityVerifyVersionProcess, during the before-transaction-commit stage:

public class EntityVerifyVersionProcess implements BeforeTransactionCompletionProcess {
    private final Object object;
    private final EntityEntry entry;

    /**
     * Constructs an EntityVerifyVersionProcess
     *
     * @param object The entity instance
     * @param entry The entity's referenced EntityEntry
     */
    public EntityVerifyVersionProcess(Object object, EntityEntry entry) {
        this.object = object;
        this.entry = entry;
    }

    @Override
    public void doBeforeTransactionCompletion(SessionImplementor session) {
        final EntityPersister persister = entry.getPersister();

        final Object latestVersion = persister.getCurrentVersion( entry.getId(), session );
        if ( !entry.getVersion().equals( latestVersion ) ) {
            throw new OptimisticLockException(
                    object,
                    "Newer version [" + latestVersion +
                            "] of entity [" + MessageHelper.infoString( entry.getEntityName(), entry.getId() ) +
                            "] found in database"
            );
        }
    }
}

The AbstractTransactionImpl.commit() method call, will execute the before-transaction-commit stage and then commit the actual transaction:

@Override
public void commit() throws HibernateException {
    if ( localStatus != LocalStatus.ACTIVE ) {
        throw new TransactionException( "Transaction not successfully started" );
    }

    LOG.debug( "committing" );

    beforeTransactionCommit();

    try {
        doCommit();
        localStatus = LocalStatus.COMMITTED;
        afterTransactionCompletion( Status.STATUS_COMMITTED );
    }
    catch (Exception e) {
        localStatus = LocalStatus.FAILED_COMMIT;
        afterTransactionCompletion( Status.STATUS_UNKNOWN );
        throw new TransactionException( "commit failed", e );
    }
    finally {
        invalidate();
        afterAfterCompletion();
    }
}

Between the check and the actual transaction commit, there is a very short time window for some other transaction to silently commit a Product price change.

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

The explicit OPTIMISTIC locking strategy offers a limited protection against stale state anomalies. This race condition is a typical case of Time of check to time of use data integrity anomaly.

In my next article, I will explain how we can save this example using an optimistic-to-pessimistic lock upgrade technique.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.