Optimistic Locking pattern for EJBs

Deploy transactionally safe EJB code

As a developer, you strive to provide both functionality and performance to your users. With that in mind, how do you ensure that your consideration for performance does not compromise data integrity?

Enterprise applications often try to balance application scalability with concurrency considerations. You can achieve scalability using many different techniques, including minimizing network traffic, which often manifests itself in the Value Object pattern. Using this pattern, however, raises data integrity issues, which we will address here.

This article describes the Optimistic Locking pattern. It begins with a transactional overview, then describes the data integrity problem in detail. Next, it considers a number of solutions, focusing on a specific solution illustrated with an example.

Transactional overview

Transactions represent a fundamental unit of work for managing business complexity. At its most basic, a transaction is an execution of a program (updating a customer record in a database or making a reservation, for example). A typical business application has many associated transactions.

Transactions ensure the integrity of an application's business rules, a process that gives rise to transaction processing (TP) applications that execute multiple transactions simultaneously. A TP application creates, executes, and manages transactions, and also provides a scalable, distributed environment in which they can run.

As a TP application example, consider an airline reservation system. (One of the first TP applications was an airline reservation system called Sabre built by IBM for American Airlines.) In our example, when a passenger wants to book a seat on a flight, three things must happen:

  • An open seat must exist on the flight
  • A ticket must be issued
  • The passenger must be billed via a credit card using Electronic Data Interchange (EDI)

This system quickly raises concurrency, scalability, and consistency concerns when multiple users try to make reservations on a distributed system.

To simplify the potential complexity, consider the entire flight booking process as one unit of work -- a transaction. A transaction comprises four critical properties:

  • Atomic
  • Consistent
  • Isolated
  • Durable

Together, the four critical properties produce the ACID acronym.

In the context of our example, consider a transaction atomic if it executes either completely or not at all. For example, if no seat exists on the plane, the entire transaction aborts or rolls back to the transaction's start. When a transaction rolls back, the database returns to the state it had prior to the transaction's inception.

Moving on, consider a transaction isolated if the transaction executes serially. In other words, it should appear as if the transaction runs alone with no other transaction occurring simultaneously. This guarantees data integrity.

A transaction must also be durable; a permanent record of the transaction must persist. This may sound obvious, but for optimization purposes transactional records are often kept in memory. However, the transaction cannot be considered ACID until the data is written to permanent storage.

Although second on the list, the last term of an ACID transaction to consider is consistent. A transaction ensures consistency if it is atomic, isolated, and durable. If an airplane possesses 10 seats and each seat sells for 00, then at the end of 10 successful transactions the airline's account should have ,000 more than it did when it started. If this is the case, the database is in a consistent state.

Transactions in EJBs

EJBs specifically support distributed transactions. Two types of transaction handling exist: bean-managed and container-managed transactions. The two types simplify the deployment of transactionally aware EJBs.

Bean-managed transactions in EJBs are controlled via the Java Transactional API (JTA) and explicated with the javax.transaction.UserTransaction interface -- the visible part of JTA.

The UserTransaction interface represents a contract between the client and the transactional coordinator. Explicitly, this involves three UserTransaction-defined methods:

  • begin()
  • commit()
  • rollback()

You can demark EJB transactions in three ways:

  1. By the client
  2. By an EJB
  3. By a container

Demark by the client

A client using an EJB can explicitly demark the transaction. All the operations called between the begin() and commit() methods are involved in the transaction. In the event that any one of these operations fails, the transaction will roll back and the database will return to its initial state. Otherwise, the transaction will commit and the changes become permanent.

Demark by an EJB

As mentioned above, transaction in EJBs can be bean managed or container managed.

You can only use bean-managed transactions with session beans. Session beans use a technique similar to that of the client-managed transactions, described above, to explicitly demark transactional boundaries. Stateless session beans are restricted because they can demark transactions only within a single method, whereas stateful session beans can demark transactions across multiple methods.

Demark by a container

Both entity beans and session beans can demark transactions with container-managed transactions. In this case, you define a method's transaction attributes in an XML deployment file. This is the preferred way to define transactional behavior with EJBs. You can manage changes in transactional behavior by modifying deployment descriptors, thus minimizing propagation of code changes. There are six different transactional attributes that can define a method's transactional behavior:

  • Not supported
  • Supports
  • Required
  • Requires new
  • Mandatory
  • Never

For a full description of each attribute, see the EJB Specification in Resources. For the purposes of this article, we'll explain the Required attribute. Bean methods marked with Required must be invoked within a transaction's scope. If a transaction already exists (that is, the method is called from a client as part of an existing transaction), the Required bean method is included in the scope of the current transaction. If it is not called within the context of a transaction, then a new transaction scope is created and the Required bean method is added to that new transaction.

The problem

Optimistic data locking relies on the idea that data remains unmodified while it is away from the server. As a simple example, consider how you'd update client details. Customer details live in a row of a database (to a first approximation) and are represented by an entity bean. If a client wants to update these details in an manner that cuts down on network traffic, all the clients' details will be packaged inside a state holder object and sent back as a serialized object. This is known as the Value Object pattern. The data is not locked and other clients can have access to it simultaneously, thus ensuring a scalable system.

The problem: the customer details are away from the source and can possibly become stale. For example, a second client can request the same customer details, modify them, and commit them back to the entity beans, which will flush the data to the database. The first client, unaware that it is dealing with a stale copy of the data, modifies and commits the data. Obviously, with no checking mechanism to detect this conflict, the first client's changes, which commit last, will be made permanent, thus overwriting the changes made by the second client.

For optimistic locking to work effectively, you must be able to detect these write-write conflicts and to make the client aware of them so they can be dealt with appropriately.

Transaction solution

In designing distributed multiuser applications, sharing objects in real time is essential, but it can lead to resource sharing conflicts. In such conflicts, a user or process can change an object's state while another user or process is using the object. Database managers solve this problem using various locking strategies.

You can employ one of two types of locking strategies: pessimistic or optimistic locking. With pessimistic locking, data is locked on the database when read for update. This can lead to high lock contention. In contrast, optimistic locking lets different transactions read the same state concurrently and checks data integrity only at update time.

A long transaction performs a series of DBMS commands that extend over a long period -- hours, days, weeks, or even months. A short transaction, in contrast, resolves a group of DBMS commands within a few seconds. For applications built with EJBs, short transactions are the default model. Long transactions are possible using EJBs, but an application using them can no longer take advantage of EJB's built-in transaction management. Instead, the application must use client-initiated transactions and the UserTransaction interface to manage its own transactions, thus paying penalties in system scalability.

However, the short transaction model can simulate long transactions using either a pessimistic or optimistic locking strategy.

Transactions concerned only with simulating an exclusive lock on a shared data object use the pessimistic locking strategy. In contrast, optimistic locking involves checking data integrity only at update time. The second strategy proves most effective and acts as the basis for our discussion.

Optimistic locking support for entities is not part of the EJB specifications. Some applications servers support optimistic locking, but it's a mistake to rely on any one mechanism. A portable solution is definitely preferable.

So how does an application detect write-write conflicts? Before trying to commit changes, an application must reread the objects read earlier and determine whether they have been changed in the interim. At least three techniques determine whether objects have been changed:

  • Timestamps
  • Version count
  • State comparison

Timestamps

If you use timestamps, an object's state information is expanded to contain a field for the timestamp. As part of the commit process, the application obtains a fresh timestamp and updates this field. So, when the object's state is committed into the database, the timestamp field indicates the commit time. To detect a write-write conflict, the application rereads the object and compares the timestamp in the newly read object with the timestamp in the originally read copy. If they are the same, no write-write conflict exists. If they are different, a conflict does exist and you must take remedial action. This works only if every application that can change the object also changes the timestamp.

Another technique: rather than rereading the object, add the timestamp field along with the primary key to the WHERE clause used in the UPDATE statement. If no rows are updated, then the row no longer matches the timestamp (or the primary key was deleted). This is more efficient than two I/Os to the database.

Version count

The version counter technique is similar to the timestamp technique. The object contains a field for the version. Each time the object successfully commits, the version is incremented. When preparing for the commit, the application rereads the objects and compares the current version value with the one in its original copy of the object. Again, all applications that can change the object must increment the version number when they do so.

State comparison

Of the three, the state comparison proves the most complex. The object is reread and then the state of the newly reread object is compared with the original copy. As this might involve comparing numerous fields, it is therefore more expensive than the previous techniques. Additionally, for state comparison to work, the application must keep its original copy of the object unchanged. This further complicates the application since it must now accumulate changes to the object in some other container and apply them at commit time. For these reasons, we recommend timestamps or counters for most situations.

Our solution uses a version counter applied to the simple Customer capture and update example previously developed in "Add XML to Your J2EE Applications" (JavaWorld, February 2001).

A detailed example

To illustrate our solution we will use part of the "Add XML to Your J2EE Applications" case study in which we developed a software application that automates a car rental. We will again employ the excellent open source application server JBoss (now at version 2.2, with version 3.0 in production) with embedded Tomcat (version 3.2.1). Our objective: concentrate on the application's customer adding/updating section. This is a mini use case embedded in the reserve-car use case from the previous article.

Under the reserve-car use case, the customer first calls the reservation desk to make a rental reservation. The rental reservation agent (RRA) takes the customer's name and queries the system for a match. If the customer is new, the RRA takes the customer's information and enters it into the system. Otherwise, existing customer details are presented for update. If the user chooses to continue, existing details are added or updated as required. This getting and setting of the customer object, which involves the two dialogs, is illustrated in Figures 1 and 2.

Figure 1. Get a customer

Then enter and submit the customer information, including name, address, and credit card number.

Figure 2. Set a customer

Having chosen the version counter mechanism to implement our optimistic locking solution, we must revise the design of the Customer entity bean. Additionally, the Customer EJB design is refactored to use the Business Interface pattern (see Resources) to make the solution clearer (see Figure 3).

Figure 3. Business Interface pattern

The EJB specification forces the bean class to provide an implementation for all methods declared in the remote interface. The specification also recommends against having the bean class directly implement the remote interface using the implements Java keyword. The EJB compiler finds any inconsistencies between the remote interface and the bean implementation.

Many errors uncovered by EJB compilation involve methods declared in the remote interface that aren't declared correctly in the bean class. The compiler could catch this in the first compilation step if the bean class could implement the remote interface directly. The Java compiler could then use the standard rules for enforcing the implementation of an interface.

The Business Interface pattern solves this problem. Both the remote interface and the bean class implement the business interface. The business interface is defined just like the bean's normal remote interface; the only difference is that it does not extend javax.ejb.EJBObject. The business interface declares the methods that can be called on the business object, but doesn't make any assumptions about the underlying implementation.

A client then interacts with the business interface, not the remote interface. This means that you can work with a remote bean, or a serializable "state" object, or with something in between. It doesn't matter to the client as long as the interface remains the same. As shown in Figure 3, we separate out the business implementation code from the EJB's technical contract. Note the following points about the Business Interface pattern as applied to this example:

  • The customer interface Business Interface defines all required business methods. No entity bean lifecycle methods reside here.
  • The Versionable interface defines our public methods for the optimistic locking solution. It is a separate interface because we can reuse it on any entity bean requiring this functionality. Not all entity beans participate in concurrent access; for example, the reservation entity bean is created only once and has no concurrency issues.
  • The CustomerObject (see the code below) class implements the Customer and Version interfaces. Until now, we have not mentioned EJBs, so in effect you could use this class on its own. This implementation bean implements the business interface, and then is extended by the actual EJB.
  • At this point, you should create the EJB-specific implementation that implements the EJB's technical contract.
  • Finally, the factory methods in the CustomerHome interface return a CustomerRemote that extends the Customer interface.

Here's the code:

CustomerObject

package com.valtech.bootcamp.carRental.business.customer;
import com.valtech.bootcamp.carRental.business.customer.Customer;
import com.valtech.bootcamp.carRental.business.customer.CustomerStateHolder;
import com.valtech.bootcamp.carRental.exceptions.ModifiedException;
import com.valtech.bootcamp.carRental.business.shared.*;
/**
 * This object implements all the business methods defined
 * in the contract with the Customer interface and also
 * provides implementations of the methods defined in the
 * Versionable interface.
 */
public class CustomerObject implements Customer, Versionable
{
   /**
    * the customer address
    */
   public String address;
   /**
    * the customer name
    */
   public String name;
   /**
    * the version of the customer, i.e the version number in 
    * the customer table
    */
   public int version;
   /**
    * the card number of the customer
    */
   public String cardNo;
   /**
    *CustomerObjectconstructor comment.
     */
   public CustomerObject()
   {
      super();
   }
   /**
    * returns a CustomerStateHolder containing all Customer attributes
    */
   public CustomerStateHolder getCustomerStateHolder()
   {
      System.err.println(version);
      CustomerStateHolder csh = new CustomerStateHolder(name, address, cardNo ,version);
      return csh;
   }
   /**
    * returns the name of the customer
    */
   public String getName()
   {
      return name;
   }
   /**
    * sets the name
    */
   public void setName(String name)
   {
       this.name = name;
   }
   /**
    * returns the version number of the customer
    */
   public int getVersion()
   {
       return version;
   }
   /**
    * sets the version number of the customer
    */
   public void setVersion(int newVersionNumber)
   {
       version = newVersionNumber;
   }
   /**
    * Setter for the address
    */
   public void setAddress(String address)
   {
      this.address = address;
   }
   /**
    * Setter for the card number
    */
   public void setCardNo(String cardNo)
   {
      this.cardNo = cardNo;
   }
   /**
    * This method does the work.
    * First it compares the version number of the value
    * the object is currently working with and the version number that the
    * entity bean is working with.
    * If these values differ, we are in the situation where we are
    * attempting to update data that has changed since we initially read the
    * data.
    * If the values are the same, we are working with data that has not
    * been updated since we started working with it. In this situation the
    * value of the version number is incremented and the entity bean
    * is updated.
    *
    * @param customerStateHolder The value object that updates
    * the details of the customer
    */
   public void setCustomerStateHolder(CustomerStateHolder customerStateHolder)
   {
      /**
       * The comparison is performed here. If the version numbers differ,
       * the Modified Exception is thrown.
       */
      if(customerStateHolder.getVersion()!= this.getVersion())
      {
         throw new ModifiedException("Data has been modified before your change has been commited");
      }
      /**
       * Otherwise we can proceed to extract the data from the value object
       * and update the version number.
       */
      else
      {
         setName(customerStateHolder.getName());
          setAddress(customerStateHolder.getAddress());
          setCardNo(customerStateHolder.getCardNo());
         int currentversion=this.getVersion();
         int newvnum =  currentversion  + 1;
         setVersion(newvnum);
         /**
          * Reset the version number to the new version number
          */
         customerStateHolder.setVersion(newvnum);
      }
    }
}

The sequence diagram in Figure 4 shows the customer-update use case. The bean is updated with the latest screen information using the setter methods in the CustomerStateHolder bean. Finally, the customer update is initiated by calling the setCustomer() method, which in turn delegates to the setCustomerStateHolder() method in the Customer entity EJB.

Figure 4. Set a customer

The code first gets the version attribute from the database and compares it to that passed in the CustomerStateHolder for update. If the comparison proves unsuccessful, an exception is thrown and no update is performed. Upon a successful comparison, the version count is incremented and this, along with the other Customer attributes, is updated with the latest information. Note that the reading and updating of the version field occurs within a transaction, thus ensuring an exclusive lock on the customer object; otherwise, some other user could update the version in between the version number's read and write to the database. This is done by demarking the setCustomerStateHolder() method with the required transaction attribute, as seen in EJB-Jar file.

In this way the entire customer update process proceeds within a transaction. The required attribute ensures that if a transaction were not started before the setCustomerStateHolder() is called, a new transaction would be initiated.

Get optimistic!

In this article, you've seen how the Optimistic Locking pattern -- complemented by the Business Interface pattern to decouple the business logic from the technical contract -- can produce a concurrent, scalable solution. Most importantly this portable solution does not depend on any particular application server implementation. You can easily adapt our solution to use other optimistic locking strategies such as timestamps or state comparisons. So get to it!

The code

You can download the complete source code for this article from Resources. It was developed in an extreme programming environment using iterative, use-case driven and risk-driven development techniques, unit testing, and pair programming. All code was developed on a Windows platform using CVS (on a Linux platform) as a software configuration management tool. The project used TortoiseCVS, an excellent CVS client that integrates directly into Windows Explorer.

You can find the code in the src directory, which includes two packages: the com package (for the source code) and a corresponding test package. We test with the JUnit testing framework. You can build the whole project with the Ant 1.2 Java-based build tool; the build file is also located in the src directory. The README file in the parent directory details the environment needed to compile the source code. The result is an .ear file you can drop into JBoss's deploy directory.

The code has also been released as an open source project and can be, found at http://jaba.sourceforge.net/, where you will find any code updates and a How-To on setting up Cocoon with JBoss/Tomcat (needed to run the examples).

Yasmin Akbar-Husain has been working as a senior consultant at Valtech since January 1999. She specializes in enterprise-scale distributed object systems, focusing on CORBA and J2EE architectures. Before coming to Valtech, she worked at large financial institutions where she defined software architectures and built and implemented frameworks and applications using Java, Smalltalk, and CORBA. She earned a B.S. in computer science in 1977 from the University of Essex and also holds a master's degree in computing. Eoin Lane has been working as a senior consultant and trainer at Valtech since early 2000. Prior to Valtech, in the mid-1990s he founded the intranet document company InConn Technologies, which provides intranet document management systems. With a PhD in physics and a background as a postdoctoral researcher and 10 years' experience in the IT industry, over the past three years Eoin has focused on Linux, object-oriented design, Java, J2EE, EJBs, and XML, with a particular interest in open source technologies.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies