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

Как устранить исключение OptimisticLockException в JPA и спящем режиме

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

Повторяемые чтения на уровне приложения подходят для предотвращения потерянных обновлений в веб-разговорах. Включение оптимистичной блокировки на уровне сущности довольно просто. Вам просто нужно пометить одно свойство логических часов (обычно целочисленный счетчик) аннотацией JPA @Version , а Hibernate позаботится об остальном.

Оптимистическая блокировка удаляет все входящие изменения, относящиеся к более старой версии сущности. Но все имеет свою цену, и оптимистичная блокировка не имеет значения.

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

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

Допустим, у нас есть следующая сущность продукта:

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

Последовательность операторов 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, version integer 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, version, id) values (?, ?, ?, ?, ?, ?, ?)][Plasma TV,0,TV,199.99,7,0,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_, optimistic0_.version as version7_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_, optimistic0_.version as version7_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_, optimistic0_.version as version7_0_0_ from product optimistic0_ where optimistic0_.id=?][1]} 

#Alice updates the product
Query:{[update product set description=?, likes=?, name=?, price=?, quantity=?, version=? where id=? and version=?][Plasma TV,0,TV,199.99,6,1,1,0]} 

#Bob updates the product
Query:{[update product set description=?, likes=?, name=?, price=?, quantity=?, version=? where id=? and version=?][Plasma TV,1,TV,199.99,7,1,1,0]} 
c.v.h.m.l.c.OptimisticLockingOneRootOneVersionTest - Bob: Optimistic locking failure
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.vladmihalcea.hibernate.masterclass.laboratory.concurrency.OptimisticLockingOneRootOneVersionTest$Product#1]

#Vlad updates the product
Query:{[update product set description=?, likes=?, name=?, price=?, quantity=?, version=? where id=? and version=?][Plasma HDTV,0,TV,199.99,7,1,1,0]} 
c.v.h.m.l.c.OptimisticLockingOneRootOneVersionTest - Vlad: Optimistic locking failure
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.vladmihalcea.hibernate.masterclass.laboratory.concurrency.OptimisticLockingOneRootOneVersionTest$Product#1]

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

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

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

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

#create tables
Query:{[create table product (id bigint not null, description varchar(255) not null, name varchar(255) not null, price numeric(19,2) not null, version integer not null, primary key (id))][]}
Query:{[create table product_liking (likes integer not null, version integer not null, product_id bigint not null, primary key (product_id))][]} 
Query:{[create table product_stock (quantity bigint not null, version integer not null, product_id bigint not null, primary key (product_id))][]} #insert product
Query:{[alter table product add constraint UK_jmivyxk9rmgysrmsqw15lqr5b  unique (name)][]} Query:{[insert into product (description, name, price, version, id) values (?, ?, ?, ?, ?)][Plasma TV,TV,199.99,0,1]} 
Query:{[alter table product_liking add constraint FK_4oiot8iambqw53dwcldltqkco foreign key (product_id) references product][]} Query:{[insert into product_liking (likes, product_id) values (?, ?)][0,1]} 
Query:{[alter table product_stock add constraint FK_hj4kvinsv4h5gi8xi09xbdl46 foreign key (product_id) references product][]} Query:{[insert into product_stock (quantity, product_id) values (?, ?)][7,1]} 

#insert product
Query:{[insert into product (description, name, price, version, id) values (?, ?, ?, ?, ?)][Plasma TV,TV,199.99,0,1]}
Query:{[insert into product_liking (likes, version, product_id) values (?, ?, ?)][0,0,1]} 
Query:{[insert into product_stock (quantity, version, product_id) values (?, ?, ?)][7,0,1]} #Alice selects the product

#Alice selects the product
Query:{[select optimistic0_.id as id1_0_0_, optimistic0_.description as descript2_0_0_, optimistic0_.name as name3_0_0_, optimistic0_.price as price4_0_0_, optimistic0_.version as version5_0_0_ from product optimistic0_ where optimistic0_.id=?][1]} 
Query:{[select optimistic0_.product_id as product_3_1_0_, optimistic0_.likes as likes1_1_0_, optimistic0_.version as version2_1_0_ from product_liking optimistic0_ where optimistic0_.product_id=?][1]}
Query:{[select optimistic0_.product_id as product_3_2_0_, optimistic0_.quantity as quantity1_2_0_, optimistic0_.version as version2_2_0_ from product_stock optimistic0_ where optimistic0_.product_id=?][1]}

#Bob selects the product
Query:{[select optimistic0_.id as id1_0_0_, optimistic0_.description as descript2_0_0_, optimistic0_.name as name3_0_0_, optimistic0_.price as price4_0_0_, optimistic0_.version as version5_0_0_ from product optimistic0_ where optimistic0_.id=?][1]} 
Query:{[select optimistic0_.product_id as product_3_1_0_, optimistic0_.likes as likes1_1_0_, optimistic0_.version as version2_1_0_ from product_liking optimistic0_ where optimistic0_.product_id=?][1]}
Query:{[select optimistic0_.product_id as product_3_2_0_, optimistic0_.quantity as quantity1_2_0_, optimistic0_.version as version2_2_0_ from product_stock optimistic0_ where optimistic0_.product_id=?][1]}

#Vlad selects the product
Query:{[select optimistic0_.id as id1_0_0_, optimistic0_.description as descript2_0_0_, optimistic0_.name as name3_0_0_, optimistic0_.price as price4_0_0_, optimistic0_.version as version5_0_0_ from product optimistic0_ where optimistic0_.id=?][1]} 
Query:{[select optimistic0_.product_id as product_3_1_0_, optimistic0_.likes as likes1_1_0_, optimistic0_.version as version2_1_0_ from product_liking optimistic0_ where optimistic0_.product_id=?][1]}
Query:{[select optimistic0_.product_id as product_3_2_0_, optimistic0_.quantity as quantity1_2_0_, optimistic0_.version as version2_2_0_ from product_stock optimistic0_ where optimistic0_.product_id=?][1]}

#Alice updates the product
Query:{[update product_stock set quantity=?, version=? where product_id=? and version=?][6,1,1,0]} 

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

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

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

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

Разбиение более крупного объекта на несколько дочерних объектов может помочь вам масштабировать обновления, снижая вероятность оптимистичных сбоев блокировки. Если вы опасаетесь возможных проблем с производительностью (из-за фрагментации состояния сущности), вам следует знать, что Hibernate предлагает несколько методов оптимизации для преодоления побочного эффекта рассеянной информации о сущности.

Вы всегда можете объединить все вложенные сущности в одном SQL-запросе, если вам нужны все данные, связанные с сущностями.

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

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