DatabaseManage Concurrent Access to JPA Entity with Locking

Manage Concurrent Access to JPA Entity with Locking

Introduction

Database transactions are notoriously messy in managing data integrity. Systems for reservations, banking, credit card processing, stock markets, etc. require high availability and fast response time for hundreds of concurrent users. With the surge of online transactions, developers now have to deal with it more often. Applications dealing with simultaneous transactions should be equipped to handle such a situation efficiently. As we know, ‘Isolation’ is the fundamental property of ‘Transaction’ but to manage one is an issue of concern. Without proper management there is the possibility of getting unwarranted results from concurrent transactions due to overlapped CRUD operations. Locking mechanism is an efficient way out of the situation. This mechanism surfaces at different levels. Many popular DBMS packages have their own provisions; JPA is no exception, providing some of the capability of inherent locking mechanism in harnessing the situation.

Locking and its Types

Locking is one of the basic techniques used to control concurrent execution of transactions. This mechanism is actually pretty simple. A lock is like a status variable associated with a data item with respect to possible operations applied to it. This lock is used as a means of synchronizing the access by concurrent transaction to the database item. Classical locking mechanisms have numerous ways of implementation at the database-level but JPA supports two types of locking mechanisms at the entity-level: optimistic model and pessimistic model.

An Example

The code below demonstrates a database transaction without enabling the locking feature. The code snippet down the line will show how to enable this in POJO under JPA.

Listing 1: Entity class

package org.mano.dto;
 
//... import statements
 
@Entity
@NamedQuery(name="findAllAccount", query="SELECT a FROM Account a")
public class Account implements Serializable{
        @Id
        private Long accountNumber;
        private String name;
        @Temporal(TemporalType.DATE)
        private Date createDate;      
        private Float balance;
        
        //... constructors, getters and setters, toString method
}
 
 

Listing 2: Service class

 
package org.mano.service;
 
//... import statements
 
public class AccountService {
 
protected EntityManager entityManager;
 
public AccountService(EntityManager entityManager) {
                this.entityManager = entityManager;
}
 
public Account createAccount(Long accno, String name, Date createDate, Float balance) {
                Account a = new Account();
                a.setAccountNumber(accno);
                a.setName(name);
                a.setCreateDate(createDate);
                a.setBalance(balance);
                entityManager.persist(a);
                return a;
        }
 
        public void closeAccount(Long accno) {
                Account a = findAccount(accno);
                if (a != null) {
                        entityManager.remove(a);
                }
        }
 
        public Account deposit(Long accno, Float amount) {
                Account a = entityManager.find(Account.class, accno);
                if (a != null) {
                        a.setBalance(a.getBalance() + amount);
                }
                return a;
        }
 
        public Account withdraw(Long accno, Float amount) {
                Account a = entityManager.find(Account.class, accno);
                if (a != null) {
                        if (a.getBalance() >= amount)
                               a.setBalance(a.getBalance() - amount);
                }
                return a;
        }
 
        public Account findAccount(Long accno) {
                return entityManager.find(Account.class, accno);
        }
 
        public List<Account> listAllAccount() {
                TypedQuery<Account> query = entityManager.createNamedQuery(
                               "findAllAccount", Account.class);
                return query.getResultList();
        }
}
 
 

Listing 3: Testing class

 
package org.mano.app;
 
//... import statements
 
public class Main {
 
public static void main(String[] args) {
        EntityManagerFactory factory = Persistence
                               .createEntityManagerFactory("JPALockingDemo");
        EntityManager manager = factory.createEntityManager();
 
        AccountService service = new AccountService(manager);
 
        manager.getTransaction().begin();
        Account a1 = service.createAccount(123l, "John Travolta", new Date(),
                               4000.00f);
        manager.getTransaction().commit();
        System.out.println("Persisted: " + a1.toString());
 
        a1 = service.findAccount(123l);
        System.out.println("Found: " + a1.toString());
 
        List<Account> list = service.listAllAccount();
        for (Account a : list) {
                System.out.println("Found list " + a.toString());
        }
 
                manager.getTransaction().begin();
                a1 = service.deposit(123l, 1000.00f);
                manager.getTransaction().commit();
                System.out.println("Deposit: " + a1.toString());
 
                manager.getTransaction().begin();
                a1 = service.withdraw(123l, 500.00f);
                manager.getTransaction().commit();
                System.out.println("Withdraw: " + a1.toString());
 
                manager.getTransaction().begin();
                service.closeAccount(123l);
                manager.getTransaction().commit();
                System.out.println("Account Closed: 123");
 
                manager.close();
                factory.close();
 
        }
 
}
 
 

Listing 4: Persistence configuration

 
<?xml version="1.0" encoding="UTF-8"?>
<persistence ...>
<persistence-unit name="JPALockingDemo" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>org.mano.dto.Account</class>
<properties>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/mybank"/>
<property name="javax.persistence.jdbc.user" value="bank"/>
<property name="javax.persistence.jdbc.password" value="secret"/>
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
</properties>
</persistence-unit>
</persistence>
 

Optimistic Locking

In optimistic locking, it is assumed that the transaction which changes/modifies the entity is engaged in isolation; in other words, it is assumed that this transaction is the only one playing with the entity currently. Obvious, this model of transaction does not acquire any locks until an actual transaction has been made, which usually is obtained at the end of the transaction. This is only possible when the query is fired to send data to the database and update at flush time. Now what happens when in the meantime some other transaction brings forth changes to the same entity? That means, flushing the transaction must be able to see whether other transactions have intervened with the commit operation. In such a situation, the current transaction simply rolls back and throws an exception called OptimisticLockException. Observe in the table below what happens when a concurrent transaction of withdraw and deposit occurs in an uncontrolled fashion.

Assume initial balance is 1000.

Time

T1

T2

Remarks

1

withdraw()

balance=balance-500

 

balance=1000-500

2

 

deposit()

balance=balance+500

balance=1000+500

3

commit(balance)

 

writes balance = 500

4

 

commit(balance)

Writes balance=1500 because when balance was read by T2 it was 1000, and  writes back 1000+500 = 1500. Previous value committed by T1 gets lost/overwritten

Note: This is a classic example of a lost update problem; there can be many other problems such as dirty read, incorrect summary, etc. found in any database literature.

We can overcome this syndrome with the help of the versioning mechanism of the JPA provider. Add a dedicated persistent property to store the version number of the entity into the database with the annotation @Version as shown below.

Listing 5: Entity class with versioning

package org.mano.dto;
 
//... import statements
 
@Entity
//...
public class Account implements Serializable{      
        @Id
        private Long accountNumber;
 
        @Version
        private Integer version;
        
        //...
        //... constructors, getters and setters, toString method
}
 

Listing 6: Optimistic lock enabled

 
public Account deposit(Long accno, Float amount) {
                Account a = entityManager.find(Account.class, accno);
                if (a != null) {
                        entityManager.lock(a, LockModeType.OPTIMISTIC);
                        a.setBalance(a.getBalance() + amount);
                }
                return a;
        }

Now, during any change, the JPA provider checks the version number; if it matches with the version number it obtained previously then changes can be applied otherwise an exception is thrown. Versioning is the basis of the optimistic locking model of JPA.  Optimistic locking is the default model.

Pessimistic Locking

In pessimistic locking, instead of waiting until the commit phase, with blind optimism that no other transaction has intervened and no change in the data item has occurred, a lock is obtained immediately. That is, the objective is to acquire the lock before starting any transaction operation. Thus it leaves no room for transaction failure due to concurrent changes; however, observe that it also leaves no room for parallel execution of transactions. In a sense it violates the principles of concurrency as there can be a lot of unused time left for some other transaction to perform in between lock obtained and lock released. But it has its use in some mission critical situations, where transaction isolation is an absolute necessity. It is also true, in real life that there are very few instances where such pessimism is required. This is one of the primary reasons why optimistic locking is the default model of JPA.

For example, to enable pessimistic WRITE_LOCK in our above application we have to invoke the function lock(a, LockModeType.PESSIMISTIC_WRITE) of EntityManager as follows.

Listing 7: Pessimistic lock enabled

public Account deposit(Long accno, Float amount) {
                Account a = entityManager.find(Account.class, accno);
                if (a != null) {
                        entityManager.lock(a, LockModeType.PESSIMISTIC_WRITE);
                        a.setBalance(a.getBalance() + amount);
                }
                return a;
        }
 
 

Conclusion

How to manage concurrent access to JPA entity is a big topic. The article gives a glimpse of the phenomenon, a basic understanding of how to implement one in JPA. Bear in mind that locking at the database-level is quite different from locking at entity-level. JPA does not replace, it rather complements the process of concurrency control mechanism. More information on locking and concurrency can be obtained from Java EE 7 tutorial documentation.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories