Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Hibernate Locking Patterns - How Does Optimistic Lock Mode Work

DZone's Guide to

Hibernate Locking Patterns - How Does Optimistic Lock Mode Work

· Java Zone
Free Resource

Microservices! They are everywhere, or at least, the term is. When should you use a microservice architecture? What factors should be considered when making that decision? Do the benefits outweigh the costs? Why is everyone so excited about them, anyway?  Brought to you in partnership with IBM.

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(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 specificLockMode.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:

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.

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 usedata integrity anomaly.

In my next article, I will explain how we can save this example using the explicit lock upgrade technique.

Code available on GitHub.

Discover how the Watson team is further developing SDKs in Java, Node.js, Python, iOS, and Android to access these services and make programming easy. Brought to you in partnership with IBM.

Topics:

Published at DZone with permission of Vlad Mihalcea, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}