Optimistic Lock в JPA

При работе с реальными системами крайне важным моментом становится взаимодействие с базой данных (далее – БД) в многопоточной среде. Другими словами, мы должны корректно обрабатывать транзакции, избегая нарушения консистентности данных во время чтения и записи данных разными потоками. Для решения проблем, которые могут возникнуть в многопоточной среде в Java Persistence API (далее – JPA) реализован механизм под названием Optimisitc Lock (далее – OL), который позволяет нескольким потокам одновременно изменять данные не мешая друг другу.

Давайте разберёмся, что же такое OL.

Для понимания этого механизма мы должны познакомиться с аннотацией @Version, которая ставится над полем в POJO классе.  Чаще всего, это выглядит так:


@Version
private Integer version;
При использовании данного механизма, каждая транзакция, которая получает доступ к данным, хранит значение его версии (например, 100500). Перед тем, как изменить эти данные,  транзакция сверяет текущую версию с полученной ранее. Если данное значение не изменилось, происходит запись данных (коммит). Если же версии не совпадают, мы получаем исключение OptimisticLockException.
После получения данного исключения транзакция “откатывается”. Данное исключение всегда содержит ссылку на сущность, с которой произошёл конфликт. Чаще всего. После получения исключения, мы пытаемся получить сущность заново и повторить операцию.
В противовес OL в JPA реализован механизм Pessimistic Lock. Это механизм обработки многопоточных транзакций имеющий несколько другую логику. Pessimistic Lock используют механизм блокировки сущности на уровне БД. Каждая транзакция может заблокировать данные. Пока блокировка не снята, ни одна другая транзакция не может получить доступ к этим данным. Соответственно, данный вид обработки многопоточного доступа к данным может снизить производительность и/или к ситуации под названием deadlock.
Для того чтобы использовать механизм OL мы должны использовать аннотацию @Version.
Например:

@Entity
public class Developer {
@Id
private Long id;
private String firstName;
private String lastName;
private String specialty;
@Version
private Long version;

...
}

Стоит помнить, что существует ряд правил использования данной аннотации:

  • В БД мы должны иметь соответствующую колонку в основной таблице сущности
  • В качестве поля для контроля версии могут быть использованы следующие типы данных:
    int, long, short, Integer, Long, Short, java.sql.Timestamp
  • Класс может иметь только одно поле с аннотацией @Version

Также, большинство реализаций JPA поддерживают механизм OL и без использования данного атрибута, но, “хорошим тоном” считается включать эту аннотацию, если мы планируем использовать OL. Если мы попробуем использовать механизм OL без аннотации @Version и наша реализация JPA не поддерживает его автоматически, то мы получим PersistenceException.

Стоит помнить, что мы должны иметь доступ для чтения данного поля, но, только JPA отвечает за изменение данного поля.

JPA поддерживает следующие режимы OL (находятся в классе LockModeType):

OPTIMISTIC
Получает блок на чтение для всех сущностей. Которые имеют аннотацию @Version
рекомендуется использовать именно его, а не синоним (READ). Как только мы делаем запрос в этом режиме, провайдер JPA исключает возможность “грязного” чтения и не повторяющегося чтения (non-repeatable).
Т.е. если другая транзакция:

  • изменила или удалила данные, но не закоммитила их
  • изменила или полностью удалила

Мы получим OptimisticLockException

READ

Синоним OPTIMISTIC (не рекомендуется к использованию)

OPTIMISTIC_FORCE_INCREMENTWRITE
Имеет такое же поведение, как и OPTIMISTIC, но, дополнительно увеличивает значение с аннотацией @Version.

WRITE

Синоним OPTIMISTIC_FORCE_INCREMENTWRITE  (не рекомендуется к использованию)

Рассмотрим некоторые примеры использования OL:


entityManager.find(Developer.class, id, LockModeType.OPTIMISTIC);


Developer developer = entityManager.find(Developer.class, id);
entityManager.lock(developer, LockModeType.OPTIMISTIC);


Query query = entityManager.createQuery("from Developer where lastName = :lastName");
query.setParameter("lastName", lastName);
query.setLockMode(LockModeType.OPTIMISTIC_INCREMENT);
query.getResultList()


@NamedQuery(name="optimisticLock", query="SELECT d FROM Developer d WHERE d.specialty LIKE :specialty", lockMode = WRITE)