Saturday, February 6, 2016

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.

Domain Class for storing Counter Information

Below is the domain class for storing the counter type and value in database table. Table will look like this -
 
Fig1. - Database Table for Counter
  1. @Entity
  2. @Table(name = "shunya_counter")
  3. public class ShunyaCounter {
  4. @Id
  5. @Enumerated(EnumType.STRING)
  6. private CounterType counterType;
  7. private long value;
  8. @Version
  9. private int version;
  10. //Getter and Setter omitted for brevity
  11. }

Approach 1. Pessimistic Locking to Generate Thread-Safe Counter

Service Class for Sequence Generation

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 UPDATEto ensure that no more than one thread increments the counter at a given time.
  1. @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
  2. public long incrementAndGetNext(CounterType counterType) {
  3. ShunyaCounter counter = dbDao.getSessionFactory().getCurrentSession().get(ShunyaCounter.class, counterType, LockMode.PESSIMISTIC_WRITE);
  4. if (counter == null) {
  5. logger.info("Inserting into counter");
  6. counter = new ShunyaCounter();
  7. counter.setCounterType(counterType);
  8. counter.setValue(0L);
  9. dbDao.getSessionFactory().getCurrentSession().saveOrUpdate(counter);
  10. }
  11. counter.setValue(counter.getValue() + 1);
  12. return counter.getValue();
  13. }

Multi-threaded Test Case for Sequence Generator

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.
  1. @Rollback(false)
  2. @Test
  3. public void testThreadSafeCounter() {
  4. shunyaCounterService.incrementAndGetNext(CounterType.MISC_PAYMENT);
  5. long t1= System.currentTimeMillis();
  6. IntStream.range(0, 100).parallel().forEach(value -> {
  7. final long nextVal = shunyaCounterService.incrementAndGetNext(CounterType.MISC_PAYMENT);
  8. logger.info("nextVal = " + nextVal);
  9. });
  10. long t2= System.currentTimeMillis();
  11. logger.info("Time Consumed = {} ms", (t2-t1));
  12. }

Approach 2. Optimistic Concurrency 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.
  1. @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
  2. public long incrementAndGetNextOptimistic(CounterType counterType) {
  3. ShunyaCounter counter = dbDao.getSessionFactory().getCurrentSession().get(ShunyaCounter.class, counterType, LockMode.OPTIMISTIC);
  4. if (counter == null) {
  5. logger.info("Inserting into counter");
  6. counter = new ShunyaCounter();
  7. counter.setCounterType(counterType);
  8. counter.setValue(0L);
  9. dbDao.getSessionFactory().getCurrentSession().saveOrUpdate(counter);
  10. }
  11. counter.setValue(counter.getValue() + 1);
  12. return counter.getValue();
  13. }

Testcase for Optimistic Approach

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.
  1. @Rollback(false)
  2. @Test
  3. public void testThreadSafeCounterOptimistic() {
  4. shunyaCounterService.incrementAndGetNext(CounterType.MISC_PAYMENT);
  5. long t1= System.currentTimeMillis();
  6. IntStream.range(0, 100).parallel().forEach(value -> {
  7. final long nextWithRetry = getNextWithRetry();
  8. logger.info("nextVal = " + nextWithRetry);
  9. });
  10. long t2= System.currentTimeMillis();
  11. logger.info("Time Consumed = {} ms", (t2 - t1));
  12. }
  13. private long getNextWithRetry() {
  14. int retryCount = 10;
  15. while(--retryCount >=0) {
  16. try {
  17. return shunyaCounterService.incrementAndGetNextOptimistic(CounterType.MISC_PAYMENT);
  18. } catch (HibernateOptimisticLockingFailureException e) {
  19. logger.warn("Mid air collision detected, retrying - " + e.getMessage());
  20. try {
  21. Thread.sleep(100);
  22. } catch (InterruptedException e1) {
  23. e1.printStackTrace();
  24. }
  25. }
  26. }
  27. throw new RuntimeException("Maximum retry limit exceeded");
  28. }
Originally posted at http://javainterviews.scribbleit.in/spring-hibernate/thread-safe-table-backed-unique-sequence-generator-spring-hibernate-ja/p/DbRXOA