Явная оптимистическая блокировка
В моем предыдущем посте я представил основные концепции блокировки сохраняемости Java.
Механизм неявной блокировки предотвращает потерю обновлений и подходит для объектов, которые мы можем активно изменять. Хотя неявная оптимистическая блокировка является широко распространенной техникой, мало кто понимает внутреннюю работу режима явной оптимистической блокировки.
Явная оптимистическая блокировка может предотвратить аномалии целостности данных, когда заблокированные объекты всегда модифицируются каким-либо внешним механизмом.
Вариант использования заказа продукта
Допустим, у нас есть следующая модель предметной области:
Наш пользователь, Алиса, хочет заказать товар. Покупка проходит следующие этапы:
- Алиса загружает объект Product
- Поскольку цена удобная, она решает заказать товар
- Пакетное задание Engine Engine изменяет цену Продукта (с учетом изменений валюты, изменений налогов и маркетинговых кампаний).
- Алиса оформляет заказ, не замечая изменения цены
Неявные недостатки блокировки
Во-первых, мы собираемся проверить, может ли механизм неявной блокировки предотвратить такие аномалии. Наш тестовый пример выглядит так:
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
|
doInTransaction( new TransactionCallable<Void>() { @Override public Void execute(Session session) { final Product product = (Product) session.get(Product. class , 1L); try { executeAndWait( new Callable<Void>() { @Override public Void call() throws Exception { return doInTransaction( new TransactionCallable<Void>() { @Override public Void execute(Session _session) { Product _product = (Product) _session.get(Product. class , 1L); assertNotSame(product, _product); _product.setPrice(BigDecimal.valueOf( 14.49 )); return null ; } }); } }); } catch (Exception e) { fail(e.getMessage()); } OrderLine orderLine = new OrderLine(product); session.persist(orderLine); return null ; } }); |
Тест генерирует следующий вывод:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
#Alice selects a Product Query:{[ select abstractlo0_. id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_. id =?][1]} #The price engine selects the Product as well Query:{[ select abstractlo0_. id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_. id =?][1]} #The price engine changes the Product price Query:{[update product set description=?, price=?, version=? where id =? and version=?][USB Flash Drive,14.49,1,1,0]} #The price engine transaction is committed DEBUG [pool-2-thread-1]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection #Alice inserts an OrderLine without realizing the Product price change Query:{[insert into order_line ( id , product_id, unitPrice, version) values (default, ?, ?, ?)][1,12.99,0]} #Alice transaction is committed unaware of the Product state change DEBUG [main]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection |
Механизм неявной оптимистической блокировки не может обнаружить внешние изменения, если только сущности также не изменены текущим контекстом постоянства. Чтобы защитить от выдачи Заказа для устаревшего состояния Продукта, мы должны применить явную блокировку к объекту Продукта.
Явная блокировка на помощь
Java Persistence LockModeType.OPTIMISTIC является подходящим кандидатом для таких сценариев, поэтому мы собираемся проверить его.
Hibernate поставляется с утилитой LockModeConverter , которая способна отобразить любой Java Persistence LockModeType на связанный с ним Hibernate LockMode .
Для простоты мы собираемся использовать специфический для Hibernate LockMode.OPTIMISTIC , который фактически идентичен его аналогу Java для персистентности.
Согласно документации Hibernate явный OPTIMISTIC Lock Mode будет:
Предположим, что транзакция (и) не будет испытывать конкуренцию для организаций. Версия объекта будет проверена ближе к концу транзакции.
Я настрою наш тестовый пример для использования явной ОПТИМИСТИЧЕСКОЙ блокировки вместо этого:
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
|
try { doInTransaction( new TransactionCallable<Void>() { @Override public Void execute(Session session) { final Product product = (Product) session.get(Product. class , 1L, new LockOptions(LockMode.OPTIMISTIC)); executeAndWait( new Callable<Void>() { @Override public Void call() throws Exception { return doInTransaction( new TransactionCallable<Void>() { @Override public Void execute(Session _session) { Product _product = (Product) _session.get(Product. class , 1L); assertNotSame(product, _product); _product.setPrice(BigDecimal.valueOf( 14.49 )); return null ; } }); } }); OrderLine orderLine = new OrderLine(product); session.persist(orderLine); return null ; } }); fail( "It should have thrown OptimisticEntityLockException!" ); } catch (OptimisticEntityLockException expected) { LOGGER.info( "Failure: " , expected); } |
Новая тестовая версия генерирует следующий вывод:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
#Alice selects a Product Query:{[ select abstractlo0_. id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_. id =?][1]} #The price engine selects the Product as well Query:{[ select abstractlo0_. id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_. id =?][1]} #The price engine changes the Product price Query:{[update product set description=?, price=?, version=? where id =? and version=?][USB Flash Drive,14.49,1,1,0]} #The price engine transaction is committed DEBUG [pool-1-thread-1]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection #Alice inserts an OrderLine Query:{[insert into order_line ( id , product_id, unitPrice, version) values (default, ?, ?, ?)][1,12.99,0]} #Alice transaction verifies the Product version Query:{[ select version from product where id =?][1]} #Alice transaction is rolled back due to Product version mismatch INFO [main]: c. v .h.m.l.c.LockModeOptimisticTest - Failure: org.hibernate.OptimisticLockException: Newer version [1] of entity [[com.vladmihalcea.hibernate.masterclass.laboratory.concurrency. AbstractLockModeOptimisticTest$Product #1]] found in database |
Поток операций выглядит так:
Версия продукта проверяется в конце транзакции. Любое несоответствие версий вызывает исключение и откат транзакции.
Риск состояния гонки
К сожалению, проверка версии на уровне приложения и принятие транзакции не являются атомарной операцией. Проверка происходит в EntityVerifyVersionProcess , на этапе перед транзакцией :
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
|
public class EntityVerifyVersionProcess implements BeforeTransactionCompletionProcess { private final Object object; private final EntityEntry entry; /** * Constructs an EntityVerifyVersionProcess * * @param object The entity instance * @param entry The entity's referenced EntityEntry */ public EntityVerifyVersionProcess(Object object, EntityEntry entry) { this .object = object; this .entry = entry; } @Override public void doBeforeTransactionCompletion(SessionImplementor session) { final EntityPersister persister = entry.getPersister(); final Object latestVersion = persister.getCurrentVersion( entry.getId(), session ); if ( !entry.getVersion().equals( latestVersion ) ) { throw new OptimisticLockException( object, "Newer version [" + latestVersion + "] of entity [" + MessageHelper.infoString( entry.getEntityName(), entry.getId() ) + "] found in database" ); } } } |
Вызов метода AbstractTransactionImpl.commit () выполнит этап before -action-commit-commit и затем зафиксирует фактическую транзакцию:
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
|
@Override public void commit() throws HibernateException { if ( localStatus != LocalStatus.ACTIVE ) { throw new TransactionException( "Transaction not successfully started" ); } LOG.debug( "committing" ); beforeTransactionCommit(); try { doCommit(); localStatus = LocalStatus.COMMITTED; afterTransactionCompletion( Status.STATUS_COMMITTED ); } catch (Exception e) { localStatus = LocalStatus.FAILED_COMMIT; afterTransactionCompletion( Status.STATUS_UNKNOWN ); throw new TransactionException( "commit failed" , e ); } finally { invalidate(); afterAfterCompletion(); } } |
Между проверкой и фактической фиксацией транзакции существует очень короткое временное окно для какой-либо другой транзакции, чтобы молча зафиксировать изменение цены Продукта.
Вывод
Явная стратегия блокировки OPTIMISTIC предлагает ограниченную защиту от аномалий устаревшего состояния. Это состояние гонки является типичным случаем времени проверки на аномалию целостности данных времени использования .
В моей следующей статье я объясню, как мы можем сохранить этот пример, используя метод explicit lock upgrade
.
- Код доступен на GitHub .
Ссылка: | Шаблоны блокировки гибернации. Как работает режим Optimistic Lock Mode от нашего партнера по JCG Влада Михалча в блоге Влада Михалча . |