In our application we have critical section of code accessed by concurrent transactions. This critical section is guarded by database lock: SELECT FOR UPDATE. And we came to an issue when updates commited by one transaction are not available in another queued concurrent transaction.
We are using Spring 3.1, Hibernate 4.2.0 , Mysql 5. Transaction isolation level is READ_COMMITTED. For querying the database we use JPA EntityManager. Please, pay attention that coming snippet extracts Hibernate session from it. We did it intentionally to demonstrate that problem is somewhere in Hibernate, probably, Hibernate cache.
Here comes the code snippet demonstrating the issue. runTransaction method is launched in two concurrent transactions started under two different Hibernate sessions. First Transaction1 is started, then Transaction2. Both of them try to acquire lock. But Transaction2 loads an object before acquiring the lock. After passing into critical section (Transaction1 is already comitted) it makes sure that the object is not in Hibernate cache and loads the object, which was updated by Transaction1. Unfortunately, it loads the version object same to one loaded before acquiring lock, so further updating fails with StaleObjectException.
Does somebody know why Transaction2 loads the old version of object after it is updated by Transaction1? We are sure, that object is updated in database by Transaction1. If we try to load it using Spring JdbcTemplate or Hibernate StatelessSession in the scope of Transaction2, update version is loaded.
Code:
private void runTransaction(EntityManager entityManager, String name) {
SessionFactory sf = ((HibernateEntityManagerFactory)emf).getSessionFactory();
Session statefullSession = sf.openSession();
System.out.println("*** Starting " + name + " ***");
statefullSession.beginTransaction();
BuildingType bt = null;
//UNCOMMENT to get javax.persistence.OptimisticLockException: org.hibernate.StaleObjectStateException
//Hibernate caches object and doesn`t notice changed made in Transaction1 later in the code
if (name.equals("Transaction2")) {
//Load object in Transaction2, but before obtaining lock
System.out.println("*** Bean just before obtaining lock in " + name + " ***");
bt = entityManager.find(BuildingType.class, 1L);
System.out.println(name + ": " + bt);
}
entityManager.createNativeQuery("SELECT count(*) FROM client_cleaner_lock FOR UPDATE").getSingleResult();
System.out.println("*** Lock in " + name + " ***");
if (name.equals("Transaction1")) {
bt = (BuildingType)statefullSession.get(BuildingType.class, 1L);
System.out.println(name + ": " + bt);
bt.setName(name);
//Make sure, that Transaction2 loads original object, unmodified by Transaction1
if (name.equals("Transaction1")) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
statefullSession.update(bt);
} else if (name.equals("Transaction2")) {
statefullSession.clear();
System.out.println(statefullSession.contains(bt));
//!!! Returns object loaded before obtaining lock in Transaction2, not updated by Transaction1 !!!
bt = (BuildingType)statefullSession.get(BuildingType.class, 1L);
System.out.println(name + ": " + bt);
bt.setName(name);
statefullSession.update(bt);
}
statefullSession.getTransaction().commit();
}
And here comes the output:
Code:
*** Starting Transaction1 ***
*** Lock in Transaction1 ***
Transaction1: Id: 1, Version: 0, Name: Apartment
*** Starting Transaction2 ***
*** Bean just before obtaining lock in Transaction2 ***
Transaction2: Id: 1, Version: 0, Name: Apartment
*** Lock in Transaction2 ***
false
Transaction2: Id: 1, Version: 0, Name: Apartment
Exception in thread "Thread-4" org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)