Sunday, June 25, 2017

JavaEE: Controlling Concurrent Access to Entity Data with Locking

On a previous post, I showed how to make a database connection and get data from a relational database using the persistence unit.  We can do all CRUD database operation.  Nevertheless, this is not good enough for any application without a transaction process.

Transaction is a sequence of operations performed as a single logical unit of work.  A logical unit of work must fulfill the four properties of a database operation to qualify as a transaction: Atomicity, Consistency, Isolation, and Durability.

I will start with a common transaction problem and explain how to prevent problems.  The pom.xml for this project is same, and an Account class definition is only added to the persistence.xml shown on another post.  It means the JTA container managed transaction is also used.
  
Account Entity
@Entity
@NamedQuery(name = "Account.findAll", query = "SELECT a FROM Account a")
public class Account implements Serializable {
   private static final long serialVersionUID = 6447459012451754056L;

   @Id
   @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "account_id_seq")
   @SequenceGenerator(name = "account_id_seq", sequenceName = "ACCOUNT_ACCOUNT_ID_SEQ")
   @Column(name = "account_id")
   private Long accountId;

   @Column(name = "account_number")
   private Integer accountNumber;

   @Min(0)
   @Column(name = "balance")
   private Integer balance;

   @Column(name = "create_time")
   private Timestamp createTime;

   @Column(name = "nick_name")
   private String nickName;

   @Column(name = "update_time")
   private Timestamp updateTime;

RESTful Web Service Endpoint
@Logging
@Path("/account")
public class AccountRestEnd {
   @Inject
   private AccountService accountService;
   
   @POST
   @Path("/balance")
   public Response changeBalance(@QueryParam("id") Long acctId,
         @DefaultValue("0") @QueryParam("amount") Integer amount){
      Account acct = accountService.updateBalance(acctId, amount);
      return Response.ok(acct).build();
   }
}

Web Service Database Operation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Logging
@Stateless
public class AccountService {
   @PersistenceContext(name="demoDB")
   private EntityManager em;
   
   public Account updateBalance(Long id, Integer amountChange){
      Account acct = em.find(Account.class, id);
      Integer currentBalance = acct.getBalance();
      acct.setBalance(currentBalance + amountChange);
      //Updating the updateTime field is omitted on purpose 
      return acct;
   }
}

Initially, I created two accounts in a database.

We can now update a balance using a post operation of http://local;host:8080/demo/account/balance?id=1&amount=8

You can run many post operations with a different amount value, and everything seems good until you realize a serious transaction problem in the production environment.  You may think the JTA manages all commit and rollback transactions including the table locking, but it is not true.

Problem with Concurrent Access
To simulate the problem of the concurrent access, let's put a breakpoint on a line 10 of the AccountService, and restart the server with a debug mode. Default debug port is a 5005.
   mvn clean package tomee:debug

Now, two POST operations are triggered to deposit 5 dollar and 10 dollars.
http://localhost:8080/demo/account/balance?id=1&amount=5
http://localhost:8080/demo/account/balance?id=1&amount=10

Now, two processes with a different server side thread will be stoped at the line 10 of the AccountService.  The currentBalance of two processes are same: 18.  Then, you can run two processes one at a time.  Depends on which process was run later, the final balance is either 23 or 28 although you deposited total 15 with two operations.

Locking
Persistence providers use two different locking mechanism: optimistic locking, which is a default, and pessimistic locking, which may be better when the underlying data is frequently accessed and modified by many transaction.  Lock modes may be specified to increase the level of optimistic locking or to request the use of pessimistic locks.

Lock Modes for Concurrent Entity Access 
Lock Mode
Description
OPTIMISTIC
Obtain an optimistic read lock for all entities with version attributes.
OPTIMISTIC_FORCE_INCREMENT
Obtain an optimistic read lock for all entities with version attributes, and increment the version attribute value.
PESSIMISTIC_READ
Immediately obtain a long-term read lock on the data to prevent the data from being modified or deleted. Other transactions may read the data while the lock is maintained, but may not modify or delete the data.
The persistence provider is permitted to obtain a database write lock when a read lock was requested, but not vice versa.
PESSIMISTIC_WRITE
Immediately obtain a long-term write lock on the data to prevent the data from being read, modified, or deleted.
PESSIMISTIC_FORCE_INCREMENT
Immediately obtain a long-term lock on the data to prevent the data from being modified or deleted, and increment the version attribute of versioned entities.
READ
A synonym for OPTIMISTIC. Use of LockModeType.OPTIMISTIC is to be preferred for new applications.
WRITE
A synonym for OPTIMISTIC_FORCE_INCREMENT. Use of LockModeType.OPTIMISTIC_FORCE_INCREMENT is to be preferred for new applications.
NONE
No additional locking will occur on the data in the database.

This table is from https://docs.oracle.com/javaee/7/tutorial/persistence-locking002.htm and this page shows how to set the lock mode. I will use one below

Pessimistic Lock to the AccountService
On the line 8 of the AccountService code, I add a LockModeType as the third parameter of the find method.  Then, put a breakpoint on the line 8 and the 10.

8
     Account acct = em.find(Account.class, id, LockModeType.PESSIMISTIC_WRITE);

Then, let's trigger two POST operations again.
http://localhost:8080/demo/account/balance?id=1&amount=9
http://localhost:8080/demo/account/balance?id=1&amount=4

Both threads stop at the line 8.  Run one thread until the line 10. Then, find another thread shown on IDE and run.  This thread hangs: The Account entity was blocked and wait.  Go to the first thread and execute the process.  After the first thread is completed, the second thread will go to the next line.

The pessimistic lock obtains a a long term lock on the data.

Optimistic Lock

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Logging
@Stateless
public class AccountService {
   @PersistenceContext(name="demoDB")
   private EntityManager em;
   
   public Account updateBalance(Long id, Integer amountChange){
      Account acct = em.find(Account.class, id);
      em.lock(acct,  LockModeType.OPTIMISTIC);
      Integer currentBalance = acct.getBalance();
      acct.setBalance(currentBalance + amountChange);
      //Updating the updateTime field is omitted on purpose 
      return acct;
   }
}

You can run the same test cases with this OPTIMISTIC Lock type, but you may get this kind of error even with one POST request:

org.hibernate.OptimisticLockException: [OPTIMISTIC] not supported for non-versioned entities [demo.model.entity.Account]

Before committing changes to the data, the persistence provider checks that no other transaction has modified or deleted the data since the data was read.  This is accomplished by a version column in the database table with a corresponding version attribute in the entity class.  The original transaction checks the version attribute and an OptimisticLockException will be thrown if the data has been modified by another transaction.  Then, the original transaction will be rolled back.

The error shown above is caused by the non-existence of the version column.  Now, I added a version_number column to the account table and added a versionNumber field with a @Version annotation to the Account entity.



   @Version
   @Column(name = "version_number")
   protected Integer versionNumber = 0;

The @Version annotation has the following requirements:
  • Only a single @Version attribute may be defined peer entity
  • The @Version attribute must be in the primary table for an entity mapped to multiple tables.
  • The attribute must be one of these: int, Integer, long, Long, short, Short, or java.sql.Timestamp

When you run the same test cases with two threads as shown with the pessimistic lock, a thread will not hang.  Since the optimistic lock is a default lacking mechanism, we don't need to specify a locking mode.  Both thread will run without blocking, but one thread will throw an exception if another thread already updated the Account object.  The exception will be thrown inside a JavaEE container.

   public Account updateBalance(Long id, Integer amountChange){
      Account acct = em.find(Account.class, id);
      Integer currentBalance = acct.getBalance();
      acct.setBalance(currentBalance + amountChange);
      //Updating the updateTime field is omitted on purpose 
      return acct;
   }

TomEE uses an Apache Geromino for the transaction implementation. A TransactionImpl class catches an OptimisticLockException in a commit method and throws an RollbackException. Another exception can be thrown during the process and you can catch an exception after the EJB AccountService method is completed.

   @POST
   @Path("/balance")
   public Response changeBalance(@QueryParam("id") Long acctId,
         @DefaultValue("0") @QueryParam("amount") Integer amount){
      Account acct = null;
      try{
         acct = accountService.updateBalance(acctId, amount);
      }catch(Exception ex){
         return Response.status(Response.Status.CONFLICT).build();
      }
      return Response.ok(acct).build();
   }
   

No comments:

Post a Comment

Java 9: Flow - Reactive Programming

Programming world has always been changed fast enough and many programming / design paradigms have been introduced such as object oriented p...