- Optimistic locking in JPA and Hibernate
- Pessimistic locking in JPA and Hibernate
The applications written today most of the time involves handling multiple users at the same time. With this requirement in place it’s mandatory to understand the possibilities for handling concurrency conflicts. I’m going to show you the tools provided by JPA and Hibernate for this purpose but note that all of these tricks can be applied to any database abstraction, you might just need to implement it manually.
Concurrency conflict
Let me show the problem through an example. Two users want to buy the very same product from an online store. For that specific product, 5 items are available at the moment. Both of the users click on Buy at the same time. Thinking about the simplistic implementation, one might fetch the data from the database and then decrease the stock count by one. For User 1, the stock count will end up with 4 which is correct. In the meantime though, User 2 also fetched the product before User 1’s update, meaning User 2 also sees stock count 5. As User 2 also wants to buy one item, the counter will be decreased here by 1 as well as for User 1. The result will be stock count of 4 which is incorrect as two users bought the same item.
This problem is also known as the Lost Update anomaly because the middle update is lost.
Lost update example
Simulating this can be done by using two concurrent transactions and updating the very same entity as shown above on the picture.
@Entity public class Product { @Id private UUID id; private String name; private int stock; // Getters and setters omitted }
@RunWith(SpringRunner.class) @SpringBootTest public class LostUpdateTest { @Autowired private TransactionalRunner txRunner; @Test public void testLostUpdate() { // given Product p = new Product("Notebook", 5); txRunner.doInTransaction(em -> { em.persist(p); }); // when txRunner.doInTransaction(em1 -> { Product p1 = em1.find(Product.class, p.getId()); txRunner.doInTransaction(em2 -> { Product p2 = em2.find(Product.class, p.getId()); p2.setStock(p2.getStock() - 1); }); p1.setStock(p1.getStock() - 1); }); // then txRunner.doInTransaction(em -> { Product product = em.find(Product.class, p.getId()); assertThat(product.getStock()).isEqualTo(4); }); } }
Handling conflicts
There are two ways to deal with concurrency conflicts, either you detect the issue and react to it or you are not letting the system to get into such situations. The first approach – detecting the anomaly – can be solved by using optimistic locking, the latter one can be done by using pessimistic locking. With using optimistic locking, when the anomaly would happen, the application just throws an exception (in case of JPA) and it can react by showing an error to the user to try again or immediately try to retry the operation in the code. In case of pessimistic locking, you are utilizing database locks to prevent concurrent readers/writers from interleaving.
Hibernate is not only an ORM which can only do mapping but it’s giving you a tool into your hands to handle conflicts in both ways.
Now I’m going to show how optimistic locking can be utilized for this purpose. Pessimistic locking will be covered in an upcoming article.
Implicit optimistic Locking in Hibernate
Versioned optimistic locking
One approach for using optimistic locking in JPA is to have a specific column for checking entity versions. Each time you update the entity the version number will be incremented and the optimistic locking check will be applied.
Setting up an entity for versioning is very easy, just add a new number attribute and annotate it with @Version .
@Entity public class VersionedProduct { @Id private UUID id; private String name; private int stock; @Version private int version; // Getters and setters omitted }
Executing the following test will result in ObjectOptimisticLockingFailureException which indicates that versioned optimistic locking is really working.
@Test(expected = ObjectOptimisticLockingFailureException.class) public void testVersionedOptimisticLocking() { // given VersionedProduct p = new VersionedProduct("Notebook", 5); txRunner.doInTransaction(em -> { em.persist(p); }); // when txRunner.doInTransaction(em1 -> { VersionedProduct p1 = em1.find(VersionedProduct.class, p.getId()); txRunner.doInTransaction(em2 -> { VersionedProduct p2 = em2.find(VersionedProduct.class, p.getId()); p2.setStock(p2.getStock() - 1); }); p1.setStock(p1.getStock() - 1); }); // then exception thrown }
Note that this exception is a Spring based exception as the framework tries to generalize all the data access exceptions. Hibernate throws StaleObjectStateException in case of optimistic locking but Spring translates it to ObjectOptimisticLockingFailureException so if you are not using Spring then you can go straight for the Hibernate exception to catch.
Although it’s very easy to use versioning on JPA entities, it’s necessary to understand how optimistic locking really works under the hood. The trick is happening when the update statement is executed for the stock change. Logging the underlying SQL statements will reveal how this is implemented.
Hibernate: update versioned_product set name=?, stock=?, version=? where id=? and version=?
- Transaction 1 loads entity by id with all the columns (including version which is 0).
- Transaction 2 loads entity by id with all the columns (including version which is 0).
- Transaction 2 modifies entity by decreasing it’s stock, it will be 4.
- Transaction 2 ends and as there is a pending change within the entity manager which is not yet propagated to the database, it will execute an update statement as shown above.
- The trick here is that in the WHERE condition, Hibernate will include a filtering condition on the version column. The value will be the initially fetched value which is 0.
- The version value will also be increased by one to indicate that the entity was updated. The version value will be 1 after Transaction 2 ends.
- Transaction 1 modifies entity by decreasing it’s stock, it will be 4 as the entity was read before Transaction 2 made the changes.
- Transaction 1 ends and as there is a pending change within the entity manager which is not yet propagated to the database, it will execute an update statement as shown above.
- The same filtering will be applied as for Transaction 2, but now because of the condition – which is version = 0 – the update statement won’t affect any rows – transaction 2 already modified the version value to 1.
- When executing update statements, the JDBC driver will return an integer value about how many rows were affected by the statement, in the current case it’s 0, indicating that some other transaction already modified the very same row and the current transaction wants to make modifications on stale data.
Non-overlapping modifications with versionless optimistic locking
Versioning works really well but there is one problem with it, what happens if the changes are not overlapping and two transactions are modifying different set of columns only, as shown on the example below.
@Test(expected = ObjectOptimisticLockingFailureException.class) public void testVersionedOptimisticLockingWithoutOverlappingChanges() { // given VersionedProduct p = new VersionedProduct("Notebook", 5); txRunner.doInTransaction(em -> { em.persist(p); }); // when txRunner.doInTransaction(em1 -> { VersionedProduct p1 = em1.find(VersionedProduct.class, p.getId()); txRunner.doInTransaction(em2 -> { VersionedProduct p2 = em2.find(VersionedProduct.class, p.getId()); p2.setName("Fancy Notebook"); }); p1.setStock(p1.getStock() - 1); }); // then exception thrown }
Transaction 2 is modifying the name of the entity and Transaction 1 is modifying the stock value. Why is there an optimistic locking exception in this case? Well, the versioning approach considers the entity as a whole, not caring about the actual set of attributes being changed.
There are two ways of dealing with this:
- Restructure your entity a little bit and extract the attributes you want to support updating in a concurrent scenario into a relation. For the test above, it’s possible to have the VersionedProduct entity which has the stock and a new entity can be created with only the name attribute and connecting them via foreign keys. However, this will solve the optimistic locking problem but will raise a performance problem because of the additional database joins.
- Versionless optimistic locking. It’s possible not to use any special version column for optimistic locking but to use only the changed attributes of the entity. This means that the modified attributes will be used in the filtering conditions to determine whether any of them has been changed in the meantime by someone else.
Setting up versionless optimistic locking is also not a rocket science. Starting from the original Product entity, it’s only needed to put up two annotations onto the class.
- @OptimisticLocking(type = OptimisticLockType.DIRTY)
- This tells Hibernate to consider only the modified attributes in the filtering condition.
- @DynamicUpdate
- This will be covered in an upcoming article more deeply but overall it allows Hibernate to rewrite the update statement dynamically.
@Entity @OptimisticLocking(type = OptimisticLockType.DIRTY) @DynamicUpdate public class VersionlessProduct { @Id private UUID id; private String name; private int stock; // Getters and setters omitted }
Executing the non-overlapping change test with the versionless product:
@Test public void testVersionlessOptimisticLockingWithoutOverlappingChanges() { // given VersionlessProduct p = new VersionlessProduct("Notebook", 5); txRunner.doInTransaction(em -> { em.persist(p); }); // when txRunner.doInTransaction(em1 -> { VersionlessProduct p1 = em1.find(VersionlessProduct.class, p.getId()); txRunner.doInTransaction(em2 -> { VersionlessProduct p2 = em2.find(VersionlessProduct.class, p.getId()); p2.setName("Fancy Notebook"); }); p1.setStock(p1.getStock() - 1); }); // then txRunner.doInTransaction(em -> { VersionlessProduct product = em.find(VersionlessProduct.class, p.getId()); assertThat(product.getName()).isEqualTo("Fancy Notebook"); assertThat(product.getStock()).isEqualTo(4); }); }
The test will result in the proper state which is that the name of the product is updated to “Fancy Notebook” and the stock is decreased to 4.
From the underlying statements behind the entity changes will clear it up what happened.
Hibernate: update versionless_product set name=? where id=? and name=? Hibernate: update versionless_product set stock=? where id=? and stock=?
As you can see the first update statement considers only the name attribute and the second update considers the stock attribute as those were the ones that are modified. As we’ve seen in the versioned optimistic locking, the same thing is happening here. If the affected row count returned from the database for the update statement is 0, that means someone else updated the same row and the attribute sets are interleaving with each other.
Summary
In this article we’ve taken a look into the problem of concurrency conflicts and having the lost update anomaly in a system which handles multiple users. The resolution of this issue can be done with either optimistic locking which detects the anomaly or pessimistic locking which prevents the anomaly from even happening.
From optimistic locking, the versioned and versionless options were covered. There is no clear winner between them which one to use, everything depends on the use-case and should be applied accordingly. Pessimistic locking will be covered in an upcoming article in the near future as well as explicit optimistic locking.
The full source code for the article can be found on my GitHub page. If you liked the article, give it a share and let me know your thoughts on Twitter.
Nice article, especially like the use of unit tests… But please give credit to Martin Kleppmann for what looks like one of his hand-drawn slides.
Thanks. Actually the picture was drawn by me from scratch. 🙂 I like Martin’s slides though.
Ah sorry, then I must praise you for opting for Martin’s hand-drawn style!
Thanks for the clear examples and unit tests. Very well prepared.
Nice Article. I tried to use these code in DataJpaTest for my repository. It’s working fine. But it’s not rollbacking the transaction that was successfull.
Hi I am unable to connect using mysql, error in sql syntax I am using optimistic locking test case.
The connection is formed entry is stored but unable to test optimistic unit test case provided.
Thanks for tutorial, Is there any article with sql ?