Enterprise Java

Hibernate locking patterns – How does Optimistic Lock Mode work

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:

productorderlineoptimisticlockmode1

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

implicitlockinglockmodenone1

  • 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(new TransactionCallable<Void>() {
	@Override
	public Void execute(Session session) {
		final Product product = (Product) session.get(Product.class, 1L);
		try {
			executeAndWait(new Callable<Void>() {
				@Override
				public Void call() throws Exception {
					return doInTransaction(new TransactionCallable<Void>() {
						@Override
						public Void execute(Session _session) {
							Product _product = (Product) _session.get(Product.class, 1L);
							assertNotSame(product, _product);
							_product.setPrice(BigDecimal.valueOf(14.49));
							return null;
						}
					});
				}
			});
		} catch (Exception e) {
			fail(e.getMessage());
		}
		OrderLine orderLine = new OrderLine(product);
		session.persist(orderLine);
		return null;
	}
});

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(new TransactionCallable<Void>() {
		@Override
		public Void execute(Session session) {
			final Product product = (Product) session.get(Product.class, 1L, new LockOptions(LockMode.OPTIMISTIC));

			executeAndWait(new Callable<Void>() {
				@Override
				public Void call() throws Exception {
					return doInTransaction(new TransactionCallable<Void>() {
						@Override
						public Void execute(Session _session) {
							Product _product = (Product) _session.get(Product.class, 1L);
							assertNotSame(product, _product);
							_product.setPrice(BigDecimal.valueOf(14.49));
							return null;
						}
					});
				}
			});

			OrderLine orderLine = new OrderLine(product);
			session.persist(orderLine);
			return null;
		}
	});
	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:

explicitlockinglockmodeoptimistic1

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.

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 the explicit lock upgrade technique.

Vlad Mihalcea

Vlad Mihalcea is a software architect passionate about software integration, high scalability and concurrency challenges.
Subscribe
Notify of
guest

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

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button