Статьи

Недостаток оптимистичной блокировки без версии

Вступление

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

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

Оптимистичная блокировка без версии

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

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

Оптимистичный тип блокировки Описание
ВСЕ Все свойства объекта будут использоваться для проверки версии объекта.
DIRTY Только текущие грязные свойства будут использоваться для проверки версии объекта
НИКТО Отключает оптимистическую блокировку
ВЕРСИЯ Суррогатная версия колонки с оптимистичной блокировкой

Для оптимистичной блокировки без версии вам нужно выбрать ALL или DIRTY.

Случай использования

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

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

optimisticlockingoneproductentitynoversion

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

1
2
3
4
5
6
7
@Entity(name = "product")
@Table(name = "product")
@OptimisticLocking(type = OptimisticLockType.DIRTY)
@DynamicUpdate
public class Product {
//code omitted for brevity
}

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

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

optimisticlockingonerootentitynoversion

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
#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]}

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

Аномалия отрывных сущностей

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

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

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

optimisticlockingonerootentitynoversionlostupdate

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

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

сращивание

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
#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 loosing an update
Query:{[update Product set price=? where id=? and price=?][1,1,21.22]}

Повторное прикрепление

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
#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 .