Статьи

Стратегия моделирования сущностей для масштабирования оптимистической блокировки

Вступление

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

Подвох

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

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

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

Время для тестирования

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

optimisticlockingoneproductentityoneversion

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

optimisticlockingonerootentityoneversion1

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

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

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

Разделяй и властвуй

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

optimisticlockingoneproductentitymultipleversions2

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

optimisticlockingonerootentitymultipleversions

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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, product_id bigint not null, primary key (product_id))][]}
Query:{[create table product_stock (quantity bigint not null, product_id bigint not null, primary key (product_id))][]}
Query:{[alter table product add constraint UK_jmivyxk9rmgysrmsqw15lqr5b  unique (name)][]}
Query:{[alter table product_liking add constraint FK_4oiot8iambqw53dwcldltqkco foreign key (product_id) references product][]}
Query:{[alter table product_stock add constraint FK_hj4kvinsv4h5gi8xi09xbdl46 foreign key (product_id) references product][]}
 
#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, product_id) values (?, ?)][0,1]}
Query:{[insert into product_stock (quantity, product_id) values (?, ?)][7,1]}
 
#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_2_1_0_, optimistic0_.likes as likes1_1_0_ from product_liking optimistic0_ where optimistic0_.product_id=?][1]}
Query:{[select optimistic0_.product_id as product_2_2_0_, optimistic0_.quantity as quantity1_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_2_1_0_, optimistic0_.likes as likes1_1_0_ from product_liking optimistic0_ where optimistic0_.product_id=?][1]}
Query:{[select optimistic0_.product_id as product_2_2_0_, optimistic0_.quantity as quantity1_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_2_1_0_, optimistic0_.likes as likes1_1_0_ from product_liking optimistic0_ where optimistic0_.product_id=?][1]}
Query:{[select optimistic0_.product_id as product_2_2_0_, optimistic0_.quantity as quantity1_2_0_ from product_stock optimistic0_ where optimistic0_.product_id=?][1]}
 
#Alice updates the product
Query:{[update product_stock set quantity=? where product_id=?][6,1]}
 
#Bob updates the product
Query:{[update product_liking set likes=? where product_id=?][1,1]}
 
#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, если вам нужны все данные, связанные с объектами .

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

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