Рубрики
Без рубрики

Как предотвратить исключение OptimisticLockException с менее оптимистичной блокировкой версии Hibernate

Автор оригинала: Vlad Mihalcea.

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

Менее оптимистичная блокировка версий-одна из менее известных функций гибернации. В этом посте я объясню как хорошие, так и плохие стороны этого подхода.

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

Чтобы поддерживать оптимистичную блокировку устаревшей схемы базы данных, Hibernate добавил механизм управления параллелизмом без версий. Чтобы включить эту функцию, вы должны настроить свои сущности с помощью аннотации @OptimisticLocking , которая принимает следующие параметры:

Все свойства сущности будут использоваться для проверки версии сущности ВСЕ
Для проверки версии сущности будут использоваться только текущие грязные свойства ГРЯЗНЫЙ
Отключает оптимистичную блокировку НИКТО
Оптимистичная блокировка столбца суррогатной версии ВЕРСИЯ

Для менее оптимистичной блокировки версий вам нужно выбрать ” ВСЕ ” или “ГРЯЗНО”.

Мы собираемся повторить вариант использования обновления продукта, который я описал в своей предыдущей статье об оптимистичном масштабировании блокировки .

Сущность продукта выглядит следующим образом:

Первое, что следует заметить, – это отсутствие столбца суррогатной версии. Для управления параллелизмом мы будем использовать оптимистичную блокировку ГРЯЗНЫХ свойств:

@Entity(name = "product")
@Table(name = "product")
@OptimisticLocking(type = OptimisticLockType.DIRTY)
@DynamicUpdate
public class Product {
//code omitted for brevity
}

По умолчанию Hibernate включает все столбцы таблицы в каждое обновление сущности, поэтому повторно использует кэшированные подготовленные инструкции. Для оптимистической блокировки грязных свойств измененные столбцы включаются в предложение update WHERE, и это является причиной использования аннотации @DynamicUpdate .

Эта сущность будет изменена тремя одновременными пользователями (например, Алисой, Бобом и Владом), каждый из которых обновит отдельное подмножество свойств сущности, как вы можете видеть на следующей диаграмме последовательности:

Последовательность инструкций SQL DML выглядит следующим образом:

#create tables
Query:{[create table product (id bigint not null, description varchar(255) not null, likes integer not null, name varchar(255) not null, price numeric(19,2) not null, quantity bigint not null, primary key (id))][]} 
Query:{[alter table product add constraint UK_jmivyxk9rmgysrmsqw15lqr5b  unique (name)][]} 

#insert product
Query:{[insert into product (description, likes, name, price, quantity, id) values (?, ?, ?, ?, ?, ?)][Plasma TV,0,TV,199.99,7,1]} 

#Alice selects the product
Query:{[select optimistic0_.id as id1_0_0_, optimistic0_.description as descript2_0_0_, optimistic0_.likes as likes3_0_0_, optimistic0_.name as name4_0_0_, optimistic0_.price as price5_0_0_, optimistic0_.quantity as quantity6_0_0_ from product optimistic0_ where optimistic0_.id=?][1]} 
#Bob selects the product
Query:{[select optimistic0_.id as id1_0_0_, optimistic0_.description as descript2_0_0_, optimistic0_.likes as likes3_0_0_, optimistic0_.name as name4_0_0_, optimistic0_.price as price5_0_0_, optimistic0_.quantity as quantity6_0_0_ from product optimistic0_ where optimistic0_.id=?][1]} 
#Vlad selects the product
Query:{[select optimistic0_.id as id1_0_0_, optimistic0_.description as descript2_0_0_, optimistic0_.likes as likes3_0_0_, optimistic0_.name as name4_0_0_, optimistic0_.price as price5_0_0_, optimistic0_.quantity as quantity6_0_0_ from product optimistic0_ where optimistic0_.id=?][1]} 

#Alice updates the product
Query:{[update product set quantity=? where id=? and quantity=?][6,1,7]} 

#Bob updates the product
Query:{[update product set likes=? where id=? and likes=?][1,1,0]} 

#Vlad updates the product
Query:{[update product set description=? where id=? and description=?][Plasma HDTV,1,Plasma TV]} 

Каждое ОБНОВЛЕНИЕ устанавливает последние изменения и ожидает, что текущий моментальный снимок базы данных будет точно таким, каким он был во время загрузки сущности. Как бы просто и прямолинейно это ни выглядело, стратегия блокировки без версий страдает одним очень неудобным недостатком.

Оптимистичная блокировка без версии возможна до тех пор, пока вы не закроете контекст сохранения. Все изменения сущностей должны происходить в открытом контексте сохранения, Спящий режим переводит переходы состояний сущностей в операторы DML базы данных.

Изменения отдельных сущностей могут сохраняться только в том случае, если сущности будут повторно управляться в новом сеансе гибернации, и для этого у нас есть два варианта:

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

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

Давайте повторим эту проблему как для слияния, так и для повторного подключения.

Слияние

Операция слияния состоит из загрузки и присоединения нового объекта сущности из базы данных и обновления его с помощью текущего снимка сущности. Слияние также поддерживается JPA , и оно терпимо к уже управляемым записям сущностей контекста сохранения. Если уже есть управляемый объект, то выбор не будет выдан, так как режим гибернации гарантирует повторяемое чтение на уровне сеанса.

#Alice inserts a Product and her Session is closed
Query:{[insert into Product (description, likes, name, price, quantity, id) values (?, ?, ?, ?, ?, ?)][Plasma TV,0,TV,199.99,7,1]} 

#Bob selects the Product and changes the price to 21.22
Query:{[select optimistic0_.id as id1_0_0_, optimistic0_.description as descript2_0_0_, optimistic0_.likes as likes3_0_0_, optimistic0_.name as name4_0_0_, optimistic0_.price as price5_0_0_, optimistic0_.quantity as quantity6_0_0_ from Product optimistic0_ where optimistic0_.id=?][1]}
OptimisticLockingVersionlessTest - Updating product price to 21.22
Query:{[update Product set price=? where id=? and price=?][21.22,1,199.99]} 

#Alice changes the Product price to 1 and tries to merge the detached Product entity
c.v.h.m.l.c.OptimisticLockingVersionlessTest - Merging product, price to be saved is 1
#A fresh copy is going to be fetched from the database
Query:{[select optimistic0_.id as id1_0_0_, optimistic0_.description as descript2_0_0_, optimistic0_.likes as likes3_0_0_, optimistic0_.name as name4_0_0_, optimistic0_.price as price5_0_0_, optimistic0_.quantity as quantity6_0_0_ from Product optimistic0_ where optimistic0_.id=?][1]} 
#Alice overwrites Bob, therefore, losing an update
Query:{[update Product set price=? where id=? and price=?][1,1,21.22]} 

Повторное подключение

Повторное подключение-это специфическая операция гибернации. В отличие от слияния, данный отдельный объект должен управляться в другом сеансе. Если есть уже загруженный объект, Hibernate выдаст исключение . Эта операция также требует выбора SQL для загрузки текущего снимка сущности базы данных. Состояние отсоединенной сущности будет скопировано в только что загруженный entitysnapshot, и механизм проверки на наличие ошибок запустит фактическое обновление DML:

#Alice inserts a Product and her Session is closed
Query:{[insert into Product (description, likes, name, price, quantity, id) values (?, ?, ?, ?, ?, ?)][Plasma TV,0,TV,199.99,7,1]} 

#Bob selects the Product and changes the price to 21.22
Query:{[select optimistic0_.id as id1_0_0_, optimistic0_.description as descript2_0_0_, optimistic0_.likes as likes3_0_0_, optimistic0_.name as name4_0_0_, optimistic0_.price as price5_0_0_, optimistic0_.quantity as quantity6_0_0_ from Product optimistic0_ where optimistic0_.id=?][1]}
OptimisticLockingVersionlessTest - Updating product price to 21.22
Query:{[update Product set price=? where id=? and price=?][21.22,1,199.99]} 

#Alice changes the Product price to 1 and tries to merge the detached Product entity
c.v.h.m.l.c.OptimisticLockingVersionlessTest - Reattaching product, price to be saved is 10
#A fresh copy is going to be fetched from the database
Query:{[select optimistic_.id, optimistic_.description as descript2_0_, optimistic_.likes as likes3_0_, optimistic_.name as name4_0_, optimistic_.price as price5_0_, optimistic_.quantity as quantity6_0_ from Product optimistic_ where optimistic_.id=?][1]} 
#Alice overwrites Bob therefore loosing an update
Query:{[update Product set price=? where id=?][10,1]} 

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

Код доступен на GitHub .