Wednesday, November 4, 2015

Differences in JPA entity locking modes

JPA provides essentially 2 types of locking mechanisms to help synchronize access to entities. Both mechanisms prevent a scenario, where 2 transactions overwrite data of each other without knowing it.

By entity locking, we typically want to prevent following scenario with 2 parallel transactions:
  1. Adam’s transaction reads data X
  2. Barbara’s transaction reads data X
  3. Adam’s transaction modifies data X, and changes it to XA
  4. Adam’s transaction writes data XA
  5. Barbara’s transaction modifies data X and changes it to XB
  6. Barbara’s transaction writes data XB
As a result, changes done by Adam are completely gone and overwritten by Barbara without her even noticing. A scenario like this is sometimes called dirty-read. Obviously, a desired result is that Adam writes XA, and Barbara is forced to review XA changes before writing XB.

How Optimistic Locking works

Optimistic locking is based on assumption that conflicts are very rare, and if they happen, throwing an error is acceptable and more convenient than preventing them. One of the transactions is allowed to finish correctly, but any other is rolled back with an exception and must be re-executed or discarded.

With optimistic locking, here is possible scenario for Adam and Barbara:
  1. Adam's transaction reads data X
  2. Barbara's transaction reads data X
  3. Adam's transaction modifies data X, and changes it to XA
  4. Adam's transaction writes data XA
  5. Barbara's transaction modifies data X and changes it to XB
  6. Barbara's transaction tries to write data XB, but receives and error
  7. Barbara needs to read data XA (or start a completely new transaction)
  8. Barbara's transaction modifies data XA and changes it to XAB
  9. Barbara's transaction writes data XAB
As you see, Barbara is forced to review Adam's changes, and if she decides, she may modify Adam's changes and save them (merge the changes). Final data contains both Adam's and Barbara's changes.

Optimistic locking is fully controlled by JPA. It requires additional version column in DB tables. It is completely independent of underlying DB engine used to store relational data.

How pessimistic locking works

For some, pessimistic locking is considered very natural. When transaction needs to modify an entity, which could be modified in parallel by another transaction, transaction issues a command to lock the entity. All locks are retained until the end of transaction and they are automatically released afterwards.
With pessimistic locks, the scenario could be like this:
  1. Adam’s transaction reads data X
  2. Adam’s transaction locks X
  3. Barbara’s transaction wants to read data X, but waits as X is already locked
  4. Adam’s transaction modifies data X, and changes it to XA
  5. Adam’s transaction writes data XA
  6. Barbara’s transaction reads data XA
  7. Barbara’s transaction modifies data XA and changes it to XAB
  8. Barbara’s transaction writes data XAB
As we can see, Barbara is again forced to write XAB, which also contains changes by Adam. However, the solution is completely different from optimistic scenario – Barbara needs to wait for Adam’s transaction to finish before even reading the data. Furthermore, we need to issue a lock command manually within both transactions for the scenario to work. (As we are not sure which transaction will be served first, Adam’s or Barbara’s, both transactions need to lock data before modifying it) Optimistic locking requires more setup than pessimistic locking, with version column needed for every entity, but then we do no need to remember issuing locks in the transactions. JPA does all the checks automatically, we only need to handle possible exceptions.

Pessimistic locking uses locking mechanism provided by underlying database to lock existing records in tables. JPA needs to know how to trigger these locks and some databases do not support completely.

Even JPA specification says, that it is not required to provide PESSIMISTIC_READ (as many DBs support only WRITE locks):
It is permissible for an implementation to use LockModeType.PESSIMISTIC_WRITE
where LockModeType.PESSIMISTIC_READ was requested, but not vice versa.

List of available lock types in JPA

First of all, I would like to say that if @Version column is provided within entity, optimistic locking is turned on by default by JPA for such entities. You do not need to issue any lock command. However, at any time, you may issue a lock with one of following lock types:
  1. LockModeType.Optimistic
    • This is really the default. It is usually ignored as stated by ObjectDB. In my opinion it only exists so that you may compute lock mode dynamically and pass it further even if the lock would be OPTIMISTIC in the end. Not very probable usecase though, but it is always good API design to provide an option to reference even the default value.
    • Example:

    LockModeType lockMode = resolveLockMode();
    A a = em.find(A.class, 1, lockMode);

      • This is a rarely used option. But it could be reasonable, if you want to lock referencing this entity by another entity. In other words you want to lock working with an entity even if it is not modified, but other entities may be modified in relation to this entity.
      • Example:
        • We have entity Book and Shelf. It is possible to add Book to Shelf, but book does not have any reference to its shelf. It is reasonable to lock the action of moving a book to a shelf, so that a book does not end up in 2 shelves. To lock this action, it is not sufficient to lock current book shelf entity, as the book does not have to be on a shelf yet. It also does not make sense to lock all target bookshelves, as they would be probably different in different transactions. The only thing that makes sense is to lock the book entity itself, even if in our case it does not get changed (it does not hold reference to its bookshelf).
      • this mode is similar to LockModeType.PESSIMISTIC_WRITE, but different in one thing: until write lock is in place on the same entity by some transaction, it should not block reading the entity. It also allows other transactions to lock using LockModeType.PESSIMISTIC_READ. The differences between WRITE and READ locks are well explained here (ObjectDB) and here (OpenJPA). However, very often, this behaves as LockModeType.PESSIMISTIC_WRITE, as the specification allows it and many providers do not implement it separately.
      • this is a stronger version of LockModeType.PESSIMISTIC_READ. When WRITE lock is in place, JPA with the help of the database will prevent any other transaction to read the entity, not only to write as with READ lock.
      • this is another rarely used lock mode. However, it is an option where you need to combine PESSIMISTIC and OPTIMISTIC mechanisms. Using plain PESSIMISTIC_WRITE would fail in following scenario:
        1. transaction A uses optimistic locking and reads entity E
        2. transaction B acquires WRITE lock on entity E
        3. transaction B commits and releases lock of E
        4. transaction A updates E and commits
      • in step 4, if version column is not incremented by transaction B, nothing prevents A from overwriting changes of B. Lock mode LockModeType.PESSIMISTIC_FORCE_INCREMENT will force transaction B to update version number and causing transaction A to fail with OptimisticLockException, even though B was using pessimistic locking.
    To issue a lock of certain type, JPA provides following means:
    You may use any of the two types of lock mechanisms in JPA. It is also possible to mix them if necessary, if you use pessimistic lock of type PESSIMISTIC_FORCE_INCREMENT.

    To learn more, read this excelent blog of Vlad Mihalcea.

    No comments:

    Post a Comment