coding-699318_960_720

В нашем блоге мы много пишем о том, как программистам организовать процесс поиска работы, и чего ждать на собеседовании. Сегодня мы начинаем цикл практических материалов, в ходе которых будем рассматривать задания, часто встречающиеся на технических интервью на позиции, связанные с разработкой на различных языках программирования. В первом выпуске — популярные вопросы по Java.

На английском этот материал доступен в блоге Cracking Java Interviews.

Как с помощью Spring и Hibernate создать потокобезопасный генератор уникальных последовательностей, поддерживающий таблицы баз данных?

Очень часто требуется создавать уникальные последовательности, управляемые СУБД для бизнес-части приложения, например, генераторы номера заказа, индентификатора, номера запроса. Учитывая, что в приложении может использоваться распределённая архитектура со множеством виртуальных Java-машин и одна база данных, невозможно использовать параллелизм на уровне виртуальных машин для обеспечения потокобезопасности генераторов последовательностей. Единственным вариантом обеспечения потокобезопасности (для решения таких проблем, как утерянные обновления) остаётся использование управления параллелизмом на уровне СУБД.

Библиотека Hibernate и даже JPA 2.0 (Java Persistence API — это спецификация Java EE и Java SE, описывающая систему управления сохранением Java-объектов в таблицы реляционных баз данных в удобном виде) предлагают два различных принципа обеспечения синхронизации на уровне баз данных: пессимистичный и оптимистичный.

При пессимистичном подходе все вызовы увеличения последовательности таблицы будут представлены последовательно, что не позволит двум потокам обновлять записи таблицы одновременно. Этот принцип хорошо подходит при частой конкуренции потоков в генераторе последовательности. В оптимистичном подходе используется поле для контроля версий записи в базе данных, которое будет увеличиваться при каждом обновлении поля потоком. Этот метод не требует блокирования полей базы данных. Высокая масштабируемость в данном случае обусловлена превалирующим количеством операций чтения общего значения большей частью потоков, в то время как запись будет производиться малым их количеством.

Для реализации потокобезопасного генератора последовательностей будут использованы оба метода, а для проверки будут использованы блок тесты.

Доменный класс для хранения информации счётчика

Ниже представлен доменный класс для хранения типа и значения счётчика в базе данных. Таблица будет иметь следующий вид:

@Entity

@Table(name = "shunya_counter")

public class ShunyaCounter {

    @Id

    @Enumerated(EnumType.STRING)

    private CounterType counterType;

    private long value;

    @Version

    private int version;

   //Getter and Setter omitted for brevity

}

Первый метод. Пессимистичное блокирование для создания потокобезопасного счётчика

Класс службы для генерации последовательности

Будет использован пессимистичный LockMode (режим блокирования записей в таблице) в библиотеке Hibernate для контроля параллелизма на уровне базы данных для обеспечения потокобезопасности параллельной генерации последовательностей. Команды LockMode.PESSIMISTIC_WRITE или LockMode.UPGRADE могут быть использованы при получении счетчика из объекта сессии, чтобы заблокировать записи таблицы базы данных. Важно, что при таком подходе нет необходимости в синхронизации на уровне виртуальных машин, поскольку база данных выполнит команду SELECT … FOR UPDATE для обеспечения увеличения счётчика одновременно не более чем одним потоком.

@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();
    }

Многопоточный тест для генератора последовательности

Будет реализован простой JUnit тест, в котором большое число потоков будет параллельно использовать генератор последовательности. Это обеспечит достоверное подтверждение верности нашего сервисного кода.

 

@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));

    }

Второй метод. Оптимистичный параллелизм при потокобезопасной генерации последовательности

Hibernate-фреймворку требуется поле версия внутри сущности для поддержки оптимистичного параллелизма, это уже было сделано в доменном классе. При желании можно определить Optimistic LockMode в методе get-сессии для того, чтобы по время коммита транзакции обязательно происходила проверка поля версии. Важно, что вызов представленного ниже метода может привести к появлению исключения — в оптимистичном параллелизме допускается изменение записей таблицы только одним потоком, поэтому при попытке одновременной записи появится ошибка.

@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();
    }

Тест для оптимистичного параллелизма

Так как в оптимистичном параллелизме генератор последовательности может выбрасывать исключения, указывающие на ошибку параллельного доступа, необходимо повторить попытку получить последовательность снова. Поэтому предыдущий тест был модифицирован с учётом этого требования.

@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");
    }

Другие публикации в блоге GMS: