How does LockModeType.OPTIMISTIC work in JPA and Hibernate

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:

ProductOrderLineOptimisticLockMode

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

ImplicitLockingLockModeNone

  • 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:

ExplicitLockingLockModeOptimistic

The Product version is checked towards 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.

If you enjoyed this article, I bet you are going to love my book 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.

Code available on GitHub.

If you liked this article, you might want to subscribe to my newsletter too.

11 thoughts on “How does LockModeType.OPTIMISTIC work in JPA and Hibernate

  1. Good article! Most discussions of locking miss out all the real-world complications & requirements, such as dependee data.

    One point, however — the actual “optimistic locking” statement & semantics thereof may vary by database dialect. I am not sure if this helps at all, but there may be some variability here (perhaps for SQL Server/ Sybase especially).

    Great writeup, hope to read more.

    1. Thanks. I don’t think the “optimistic locking” semantics varies among different database providers. The optimistic locking mechanism uses a conditional UPDATE statement (WHERE version = ?), that implies a hard-lock acquisition even when the database uses a MVCC mechanism. While MVCC allows non-blocking reader-writer and writer-reader, writers still block writers. So, once you took a lock on the row you’ve just incremented the version, the consistency guarantees must ensure that the database will reflect your current transaction version value (right after commit).

  2. Does locking an entity using OPTIMISTIC/PESSIMISTIC lock result in the entity being evicted or not being used from the second level cache the next time.

    In my case, I am using EhCache2.4.3, Hibernate4.3.7 and Derby DB version 10. In my case I am loading an entity using session.getId(..) and then locking this entity using OPTIMISTIC/PESSIMISTIC lock and not modifying the entity. Finally I commit the transaction and close the session. Subsequently I am opening second session and trying to load the same entity using session.getId(..) and I see it is resulting in DB hit. When I comment the OPTIMISTIC/PESSIMISTIC line in my code the second session now reads it from the cache. I am wondering if this is an expected behavior. I tried with both NONSTRICT_READ_WRITE and READ_WRITE concurrency strategy.

    1. If that’s how it behaves, then that’s ho it is implemented. I remember that READ_WRITE uses a Lock itself based on time-stamps, and until the lock expires all reads go to the database. Maybe it’s the same for explicit locking too, but I can’t tell for sure.

  3. Hi, I think that you forgot to change the mysql default isolation, repeatable-read, to read-committed. In repeatable-read isolation, your case is not work.

      1. I am using MySQL, and explicit optimistic locking is not working for me with the default isolation level REPEATABLE_READ (just because new database value of version cannot be re-read by EntityVerifyVersionProcess during my transaction). Once I’ve changed the level to READ_COMMITTED it’s worked as expected. Does it mean that explicit optimistic locking does not get on with READ_COMMITTED in general?

  4. Thanks Vlad for your whole effort, it’s really invaluable! Could you please also clarify how to implement explicit optimistic locking in a conversation where say two transactions span a single Hibernate session (common example with an entity edit web form). Should I obtain explicit optimistic lock in the last transaction only? Or is there a way to lock while reading the entity during the first transaction and hold that lock for the time of the whole conversation?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s