How to implement thread safe table backed global counter in spring hibernate?

Munish Chandel | July 28, 2018 at 10:08 AM | 127 views


How will you create a thread safe table backed unique sequence generator in spring + hibernate?

This is a common scenario where you want to generate a database controlled unique sequence for your application business requirement i.e order number generator, id generator, claim number generator etc. Considering that your application may have distributed architecture with multiple JVMs and a single database, we do not have option of using JVM level synchronization to ensure thread safety of sequence. The only option is to use database level concurrency control to achieve thread-safety (to resolve issues like lost updates, etc.)

Hibernate (and even JPA 2.0) offers two approaches to handle concurrency at database level:

  1. Pessimistic Approach - All the calls to increment sequence in the table will be serialized in this case, thus no two threads can update the same table row at the same time. This approach suits our scenario since thread contention is high for sequence generator.

  2. Optimistic Approach - a version field is introduced to the database table, which will be incremented every time a thread updates the sequence value. This does not require any locking at the table row level, thus scalability is high using this approach provided most threads just read the shared value and only few of them write to it.

We will use both these approaches to create a thread-safe sequence generator along with Unit Testcases to test the same.

Step 1. Create Domain model for storing Counter

Below is the domain class for storing the counter type and value in database table. Table will look like this -

database counter
Database Counter table

Creating Java Model for the same:

/src/main/java/models/ShunyaCounter.java
@Entity
@Table(name = "t_counter")
public class ShunyaCounter {
    @Id
    @Enumerated(EnumType.STRING)
    private CounterType counterType;
    private long value;
    @Version
    private int version;
   //Getter and Setter omitted for brevity
}

Step 2. Using Pessimistic Locking to generate Thread-Safe Counter

We will use pessimistic LockMode in hibernate to control concurrency at the database level to ensure thread-safe parallel sequence generation. LockMode.PESSIMISTIC_WRITE or LockMode.UPGRADE can be used while invoking get on the session object to obtain lock at database row level. Please note that no JVM level synchronization is required in this case, database will issue SELECT …​ FOR UPDATE to ensure that no more than one thread increments the counter at a given time.

Service class for sequence generation
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
public long incrementAndGetNext(CounterType counterType) {
    ShunyaCounter counter = dbDao.getSessionFactory().getCurrentSession().get(ShunyaCounter.class, counterType, LockMode.PESSIMISTIC_WRITE);
    if (counter == null) {
        logger.info("Inserting into counter");
        counter = new ShunyaCounter();
        counter.setCounterType(counterType);
        counter.setValue(0L);
        dbDao.getSessionFactory().getCurrentSession().saveOrUpdate(counter);
    }
    counter.setValue(counter.getValue() + 1);
    return counter.getValue();
}

We will write a simple JUNIT testcase to fork multiple threads, each one calling the sequence generator in parallel. This will give us fair indication of the validity of our service code.

Multi-threaded Test Case for Sequence Generator
@Rollback(false)
@Test
public void testThreadSafeCounter() {
    shunyaCounterService.incrementAndGetNext(CounterType.MISC_PAYMENT);
    long t1= System.currentTimeMillis();
    IntStream.range(0, 100).parallel().forEach(value -> {
        final long nextVal = shunyaCounterService.incrementAndGetNext(CounterType.MISC_PAYMENT);
        logger.info("nextVal = " + nextVal);
    });
    long t2= System.currentTimeMillis();
    logger.info("Time Consumed = {} ms", (t2-t1));
}

Step 3. Using Optimistic Locking to generate thread-safe Sequence

Hibernate needs a version field inside the entity to enable optimistic concurrency control, we have already added that in our domain class. Optionally we can also specify Optimistic LockMode on session’s get method to ensure that version field is checked at the time of committing the transaction. Please note here that call to below method may throw a exception if two threads tries to make parallel calls to database row update, as only one can succeed in optimistic concurrency approach.

@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
    public long incrementAndGetNextOptimistic(CounterType counterType) {
        ShunyaCounter counter = dbDao.getSessionFactory().getCurrentSession().get(ShunyaCounter.class, counterType, LockMode.OPTIMISTIC);
        if (counter == null) {
            logger.info("Inserting into counter");
            counter = new ShunyaCounter();
            counter.setCounterType(counterType);
            counter.setValue(0L);
            dbDao.getSessionFactory().getCurrentSession().saveOrUpdate(counter);
        }
        counter.setValue(counter.getValue() + 1);
        return counter.getValue();
    }

As optimistic sequence generator method can throw a exception indicating a mid air collision, we need to make retry attempts to get the sequence again, so we have modified the previous testcase to accommodate this change.

Testcase for Optimistic Approach
@Rollback(false)
@Test
public void testThreadSafeCounterOptimistic() {
    shunyaCounterService.incrementAndGetNext(CounterType.MISC_PAYMENT);
    long t1= System.currentTimeMillis();
    IntStream.range(0, 100).parallel().forEach(value -> {
        final long nextWithRetry = getNextWithRetry();
        logger.info("nextVal = " + nextWithRetry);
    });
    long t2= System.currentTimeMillis();
    logger.info("Time Consumed = {} ms", (t2 - t1));
}
private long getNextWithRetry() {
    int retryCount = 10;
    while(--retryCount >=0) {
        try {
            return shunyaCounterService.incrementAndGetNextOptimistic(CounterType.MISC_PAYMENT);
        } catch (HibernateOptimisticLockingFailureException e) {
            logger.warn("Mid air collision detected, retrying - " + e.getMessage());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        }
    }
    throw  new RuntimeException("Maximum retry limit exceeded");
}

Thats all.


Buy DRM Free PDF for Complete Collection of Interview Questions
Generic placeholder image
ebook PDF - Cracking Java Interviews v3.4 by Munish Chandel

240 real Java interview questions on core Java, concurrency, algorithms, design & data structures, spring, hibernate for Investment Bank, Healthcare IT, product and service based companies, Author : Munish Chandel, Price: 250, Type: PDF

Free Email Updates
Subscribe to Blog via Email

Enter your email address to subscribe to this blog and receive notifications of new posts by email.


Similar Articles:
  1. Prevent Lost Updates in Database Transaction using Spring Hibernate
  2. What is N+1 problem in Hibernate, how will you identify and solve it?
  3. No two threads shall pick up the same task from database table using Hibernate
  4. Prevent Order Placement if Produce Price Changes inbetween
  5. How to implement thread safe table backed global counter in spring hibernate?

This website uses cookies to ensure you get the best experience on our website. more info