Table backed global counter in spring hibernate

Upasana | August 01, 2020 | 4 min read | 1,952 views


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.


Top articles in this category:
  1. Prevent Lost Updates in Database Transaction using Spring Hibernate
  2. N+1 problem in Hibernate & Spring Data JPA
  3. Redis rate limiter in Spring Boot
  4. Disable SSL validation in Spring RestTemplate
  5. Java 8 date time JSON formatting with Jackson
  6. Feign RequestInterceptor in Spring Boot
  7. How to prevent duplicate form submission in Spring MVC

Recommended books for interview preparation:

Find more on this topic:
Buy interview books

Java & Microservices interview refresher for experienced developers.