Tuesday, 29 October 2013

Optimistic and Pessimistic and lock modes in Hibernate

First we need to understand why do we need Optimistic and Pessimistic lock in hibernate.

The first thing to note about hibernate is that, hibernate never changes the default lock system (isolation level) provided by DBMS.  This is usually read committed or repeatable read.

Although we have a ways to override the default isolation in hibernate by specifying property hibernate.connection.isolation = 4 in hibernate configuration file. This will only be useful if we have hibernate managing the transaction for your application.

Here
1 - Read Uncommitted isolation
2 - Read Commited isolation
4 - Repeatable read isolation
8 - Serializable isolation

So lets say we have read committed as our isolation level.  Read committed allows shared read locks and exclusive write locks. Which means to say, multiple transaction can read data (shared read) the same record, but if we have a transaction that update record, it provide write locks and prevent other threads from reading the same records.

Consider this case in Hibernate. We have 2 threads A and B trying to modify the same record.

Figure :















Thread A and B both tries to read same record. Since there is no read lock to the DBMS for this isolation, both the threads are able to read the record.  Since this is a hibernate application, both the threads have their own persistent context and the records is been loaded to its persistent context.

Now assume both this thread modifies the record. Since hibernate has lazy loading feature, the modification remain in the persistent context and the is not flushed until the unit of work complete.
Now assuming the thread A completes the unit of work and all the changes are been flushed to the database.  The thread A acquire the write lock and the database records is updated. Tx for A ends and write lock is released. Now when Thread B tries to update the same records, since there is no write lock acquired to the record, it is able to acquire write lock himself and update the record. So the changes made by Thread A may be lost. Hence the need for lock in Hibernate.

The Above problem can be addressed by applying Optimistic Lock.

Enabling versioning in Hibernate (Optimistic Lock)

Each entity instance has a version column, which can be a number or a timestamp.  Hibernate increments an object’s version when it’s modified, compares versions automatically, and throws an exception if a conflict is detected. Consequently, you add this version property to all your persistent entity classes to enable optimistic locking.

Example :

Java Class

public class Employee {
...

// case when we want to use Number as version column
private int version ;

// case when we want to use timestamp as version column
private Date updatedDate;

}


HBM Mapping :
<class name = 'Employee' table='employee' >
...

<!-- In case the version is of type number -->
<version name='version' access='field' column='version'>
...
<!-- In case the version is of type timeStamp -->
<timestamp name='updatedDate' access='field' column='updated_date'>

</class>


Points to Note:
  • we have not added getter and setter method for version column. This is because we want this to be managed by the application.
  • The version property in application doesn't usually have any business purpose and is used by hibernate application for opt.
In theory, a timestamp is slightly less safe because 
  • Two concurrent transactions may both load and update the same employee in the same millisecond.
  • Retrieving the current time from the JVM isn’t necessarily safe in a clustered environment, where nodes may not be time synchronized. You can switch to retrieval of the current time from the database machine with the source="db" attribute on the <timestamp> mapping. but again, this would require another database select Query to retrieve the timestamp.

How Hibernate Manages versioning :

Every DML operation that has versioning enabled performs versioning checks. For Example, consider this - Employee instance was loaded into persistent context with version id 1. We did some modification to the entity and when the persistent context is flushed to database, hibernate detects that modification and increments the version of the entity to version 2. It then executes the SQL UPDATE to make this modification persistent in the database : 

Update employee set name='john', type='employee', version='2'
where id='1' and version='1' and name='john' and type='contract' .

If another concurrent unit of work updated and committed the same row, the version column no longer contains the value 1, and the row isn’t updated (check where clause).

Points to Note :
  • Hibernate takes care of increasing the version number by itself (after update).  This is not suppose to be managed by DBMS. We can have a trigger at DBMS which would take care of increasing the version number. but it will have some disadvantage :
    1. After each update, we may have to fire select query to reload the changed version id to persistent context.
  • Hibernate checks the row count for this statement as returned by the JDBC driver—which in this case is the number of rows updated, zero—and throws a StaleObjectStateException.
  • If you want to disable automatic increment for a particular value-typed property or collection, map it with the optimistic-lock="false" attribute.


But what happens if we are working on a legacy system where adding a column in table(versioning) is not possible. In that case, hibernate has an alternative strategy as below :

This alternative implementation of versioning checks the current database state against the unmodified values of persistent properties at the time the object was retrieved (or the last time the persistence context was flushed). You may enable this functionality by setting the optimistic-lock attribute on the class

<class name="Employee" table="employee" optimistic-lock="all">
....
</class>

The following SQL is fired when we flush the Employee instance :
update employee
set name='john', department='contract'
where id=1 and name='john' and type='contract'

so if any other transaction has update the employee row after the instance was loaded in persistent context, the where clause would fail. Hence we will get StaleObjectStateException.

Alternately we can set optimistic-lock="dirty" which will only include modified properties. This means two units of work may modify the same   object concurrently, and a conflict is detected only if they both modify the same value-typed property.


Pessimistic Locking in Hibernate :

The optimistic lock will work when we are dealing with entity instance. But consider the below case :

Session session = sessionFactory.getCurrentSession();
Transaction tx = session.beginTransaction();
session.setFlushMode(FlushMode.MANUAL);

Employee employee = (Employee) session.get(Employee.class, 1l) ;
String type = (String)session.createQuery(" select e.type from Employeee e where e.id = :id")
.setParameter("id", new Long("1"))
.uniqueResult();
session.flush();
tx.commit();
session.close();

In this example, we have set Flush Mode as Manual. This will not flush the changes to the employee object when we use hibernate Query language.

Now after the employee instance was loaded to the persistent context, lets say another thread modified the type of the employee with id =1 from "contract" to "employee". So when the hibernate query was fired, the result type was modified variable. In this case, we would have 2 different values for property 'type' on the same instance same persistent context.

This example is simplified, but it’s enough to illustrate how a unit of work that mixes entity and scalar reads is vulnerable to nonrepeatable reads, if the database transaction isolation level is read committed.

Instead of switching all database transactions into a higher and nonscalable isolation level,you obtain stronger isolation guarantees when necessary with the lock() method on the Hibernate Session:


Session session = sessionFactory.getCurrentSession();
Transaction tx = session.beginTransaction();

Employee employee = (Employee) session.get(Employee.class, 1l) ;
session.lock(employee, LockMode.UPGRADE);

String type = (String)session.createQuery(" select e.type from Employeee e where e.id = :id")
.setParameter("id", new Long("1"))
.uniqueResult();
session.flush();
tx.commit();
session.close();


Using LockMode.UPGRADE results in a pessimistic lock held on the database for the employee instance. With this, no other transaction will be able to acquire lock to this employee row.
Shorten version :

Employee employee = (Employee) session.get(Employee.class, 1l,LockMode.UPGRADE) ;

Few points to Note  :

  • If some other transaction has already acquired the lock, LockMode.UPGRADE will wait till the lock is acquired. Hence Use LOCKMODE.UPDATE_NOWAIT that causes immediately failure it the lock cannot be acquired.
  • The duration of the lock held is for the single transaction .
  • If the value of the 'type' property is modified on same transaction and before the query is fired, We will not get the modified value .


Example :
session.setFlushMode(FlushMode.MANUAL);
Employee employee = (Employee) session.get(Employee.class, 1l) ;
System.out.println(employee.getType()); // Contract
session.lock(employee, LockMode.UPGRADE);
employee.setType("Employee") ; // Changed the value to Employee
String type = (String)session.createQuery(" select e.type from Employeee e where e.id = :id")
.setParameter("id", new Long("1"))
.uniqueResult();
System.out.println(type); // Contract
session.flush();
tx.commit();
session.close();

The reason is because the changed was not flushed before the query is fired. This is due to FlushMode.MANUAL. If the change was flushed before the query was executed or we had FlushMode.AUTO defined, we would get updated result.

Apart from LockMode.UPGRADE and LockMode.UPGRADE_NOWAIT, hibernate supports different lock modes:

Hibernate Lock Modes:

LockMode.NONE  : Don’t go to the database unless the object isn’t in any cache.
LockMode.READ  : Bypass all caches, and perform a version check to verify that the object in memory is the same version that currently exists in Database.
LockMode.UPDGRADE  : Bypass all caches, do a version check (if applicable), and obtain a database-level pessimistic upgrade lock.
LockMode.UPDGRADE_NOWAIT : The same as UPGRADE, but use a SELECT ... FOR UPDATE NOWAIT, if supported. This disables waiting for concurrent lock releases, thus throwing a locking exception immediately if the lock can’t be obtained.
LockMode.FORCE : Force an increment of the objects version in the database, to indicate that it has been modified by the current transaction.
LockMode.WRITE : Obtained automatically when Hibernate has written to a row in the current transaction.

No comments:

Post a Comment