Статьи

Оптимистичное управление параллелизмом на основе версий в JPA / Hibernate

Эта статья представляет собой введение в оптимистичный контроль параллелизма на основе версий в Hibernate и JPA. Эта концепция довольно старая, и на ней много написано, но в любом случае я видел ее заново, неправильно понятой и неправильно использованной. Я пишу это просто для того, чтобы распространять знания и, надеюсь, вызвать интерес к теме управления и блокировки параллелизма.

Случаи применения

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

Это также может произойти в серверной среде — несколько транзакций могут изменить общую сущность, и мы хотим предотвратить такие сценарии:

  1. Транзакция 1 загружает данные
  2. Транзакция 2 обновляет эти данные и фиксирует
  3. Используя состояние, загруженное на шаге 1 (который больше не является текущим), транзакция 1 выполняет некоторые вычисления и обновляет состояние

В некотором смысле это сопоставимо с неповторяющимися чтениями.

Решение: управление версиями

По этой причине Hibernate и JPA реализуют концепцию управления параллелизмом на основе версий. Вот как это работает.

Вы можете пометить простое свойство с помощью @Versionили <version>(числовой или отметки времени). Это будет специальный столбец в базе данных. Наше отображение может выглядеть так:

@Entity
@Table(name = "orders")
public class Order {
    @Id
    private long id;
 
    @Version
    private int version;
 
    private String description;
 
    private String status;
 
    // ... mutators
}

Когда такая сущность сохраняется, свойству версии присваивается начальное значение.

Всякий раз, когда он обновляется, Hibernate выполняет запрос как:

update orders
set description=?, status=?, version=?
where id=? and version=?

Обратите внимание, что в последней строке WHEREпредложение теперь включает version. Это значение всегда устанавливается на «старое» значение, так что оно будет обновлять строку, только если она имеет ожидаемую версию.

Допустим, два пользователя загружают заказ в версии 1 и смотрят его в графическом интерфейсе.

Анна решает утвердить заказ и выполняет такое действие. Статус обновляется в базе данных, все работает как положено. Версии, переданные в оператор обновления, выглядят так:

update orders
set description=?, status=?, version=2
where id=? and version=1

Как вы можете видеть, при сохранении этого обновления слой постоянства увеличивает счетчик версий до 2.

В своем графическом интерфейсе Бетти все еще имеет старую версию (номер 1). Когда она решает выполнить обновление заказа, заявление выглядит так:

update orders
set description=?, status=?, version=2
where id=? and version=1

На данный момент, после обновления Энн, версия строки в базе данных равна 2. Так что это второе обновление затрагивает 0 строк (ничего не соответствует WHEREпредложению). Hibernate обнаруживает, что и org.hibernate.StaleObjectStateException(завернутый в javax.persistence.OptimisticLockException).

В результате второй пользователь не может выполнять какие-либо обновления, если он не обновляет представление. Для правильного пользовательского опыта нам нужна чистая обработка исключений, но я это опущу.

конфигурация

Здесь мало что можно настроить. @VersionСвойство может быть числом или отметкой о времени. Число является искусственным, но обычно занимает меньше байтов в памяти и базе данных. Временная метка больше, но она всегда обновляется до «текущей временной метки», поэтому вы можете использовать ее, чтобы определить, когда сущность была обновлена.

Зачем?

Так зачем нам это использовать?

  • Он предоставляет удобный и автоматизированный способ поддержания согласованности в сценариях, подобных описанным выше. Это означает, что каждое действие может быть выполнено только один раз, и это гарантирует, что пользовательский или серверный процесс видел современное состояние при принятии бизнес-решения.
  • Для настройки требуется очень мало работы.
  • Благодаря своей оптимистической природе, это быстро. Блокировки нигде нет, к тем же запросам добавлено еще одно поле.
  • В некотором смысле это гарантирует повторяемое чтение даже с уровнем изоляции транзакции чтения. Это закончится исключением, но, по крайней мере, невозможно создать противоречивое состояние.
  • Он хорошо работает с очень длинными разговорами, в том числе с несколькими транзакциями.
  • Он полностью согласован во всех возможных сценариях и условиях гонки в базах данных ACID. Обновления должны быть последовательными, обновление включает блокировку строки, а «вторая» всегда будет влиять на 0 строк и завершится неудачно.

демонстрация

Чтобы продемонстрировать это, я создал очень простое веб-приложение. Он связывает воедино Spring и Hibernate (за JPA API), но будет работать и в других настройках: Pure Hibernate (без JPA), JPA с другой реализацией, не-webapp, не-Spring и т. Д.

Приложение сохраняет Orderсхему со схемой, аналогичной приведенной выше, и отображает ее в веб-форме, где вы можете обновить описание и статус. Чтобы поэкспериментировать с управлением параллелизмом, откройте страницу на двух вкладках, внесите различные изменения и сохраните. Попробуйте то же самое без @Version.

Он использует встроенную базу данных, поэтому ему требуется минимальная настройка (только веб-контейнер) и требуется только перезагрузка, чтобы начать с новой базы данных.

Это довольно упрощенно — доступ EntityManagerв @Transactional @Controllerи поддерживает форму непосредственно с JPA-отображенным лицом. Возможно, это не лучший способ сделать что-то для менее тривиальных проектов, но, по крайней мере, он собирает весь код в одном месте и его очень легко понять.

Полный исходный код в виде проекта Eclipse можно найти в моем репозитории GitHub .