Вступление
В моем предыдущем посте я представил режим блокировки OPTIMISTIC_FORCE_INCREMENT, и мы применили его для распространения изменения версии дочерней сущности на заблокированную родительскую сущность. В этом посте я собираюсь рассказать о режиме блокировки PESSIMISTIC_FORCE_INCREMENT и сравнить его с его оптимистичным аналогом.
Больше похоже, чем другое
Как мы уже узнали, режим блокировки OPTIMISTIC_FORCE_INCREMENT может увеличивать версию объекта, даже если текущая транзакция не изменяет состояние заблокированного объекта. Для каждого режима блокировки Hibernate определяет связанную LockingStrategy, и событие режима блокировки OPTIMISTIC_FORCE_INCREMENT обрабатывается OptimisticForceIncrementLockingStrategy :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public class OptimisticForceIncrementLockingStrategy implements LockingStrategy { //code omitted for brevity @Override public void lock(Serializable id, Object version, Object object, int timeout, SessionImplementor session) { if ( !lockable.isVersioned() ) { throw new HibernateException( "[" + lockMode + "] not supported for non-versioned entities [" + lockable.getEntityName() + "]" ); } final EntityEntry entry = session.getPersistenceContext().getEntry( object ); // Register the EntityIncrementVersionProcess action to run just prior to transaction commit. ( (EventSource) session ).getActionQueue().registerProcess( new EntityIncrementVersionProcess( object, entry ) ); } } |
Эта стратегия регистрирует EntityIncrementVersionProcess в текущей очереди действий в контексте постоянства . Версия заблокированного объекта увеличивается до того, как завершится текущая текущая транзакция.
01
02
03
04
05
06
07
08
09
10
11
|
public class EntityIncrementVersionProcess implements BeforeTransactionCompletionProcess { //code omitted for brevity @Override public void doBeforeTransactionCompletion(SessionImplementor session) { final EntityPersister persister = entry.getPersister(); final Object nextVersion = persister.forceVersionIncrement( entry.getId(), entry.getVersion(), session ); entry.forceLocked( object, nextVersion ); } } |
Аналогично OPTIMISTIC_FORCE_INCREMENT, режим блокировки PESSIMISTIC_FORCE_INCREMENT обрабатывается PessimisticForceIncrementLockingStrategy :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
public class PessimisticForceIncrementLockingStrategy implements LockingStrategy { //code omitted for brevity @Override public void lock(Serializable id, Object version, Object object, int timeout, SessionImplementor session) { if ( !lockable.isVersioned() ) { throw new HibernateException( "[" + lockMode + "] not supported for non-versioned entities [" + lockable.getEntityName() + "]" ); } final EntityEntry entry = session.getPersistenceContext().getEntry( object ); final EntityPersister persister = entry.getPersister(); final Object nextVersion = persister.forceVersionIncrement( entry.getId(), entry.getVersion(), session ); entry.forceLocked( object, nextVersion ); } } |
Заблокированный объект сразу увеличивается, поэтому эти два режима блокировки выполняют ту же логику, но в разное время. Присвоение имен PESSIMISTIC_FORCE_INCREMENT может привести вас к мысли, что вы используете пессимистичную стратегию блокировки, в то время как в действительности этот режим блокировки является просто оптимистичным вариантом блокировки.
Пессимистическая блокировка влечет за собой явные физические блокировки (разделяемые или исключительные), тогда как оптимистическая блокировка полагается на неявную блокировку текущего уровня изоляции транзакций.
Вариант использования репозитория
Я собираюсь повторно использовать предыдущее упражнение и переключиться на использование режима блокировки PESSIMISTIC_FORCE_INCREMENT . Напомним, что наша модель предметной области содержит:
- объект репозитория , чья версия увеличивается с каждым новым коммитом
- объект Commit , инкапсулирующий один атомарный переход состояния репозитория
- компонент CommitChange , инкапсулирующий одно изменение ресурса репозитория
Профилактика одновременных изменений
К нашей системе одновременно обращаются как Алиса, так и Боб. Сущность Repository всегда блокируется сразу после ее извлечения из базы данных:
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
37
38
39
40
41
42
43
44
45
46
47
48
49
|
private final CountDownLatch startLatch = new CountDownLatch( 1 ); private final CountDownLatch endLatch = new CountDownLatch( 1 ); @Test public void testConcurrentPessimisticForceIncrementLockingWithLockWaiting() throws InterruptedException { LOGGER.info( "Test Concurrent PESSIMISTIC_FORCE_INCREMENT Lock Mode With Lock Waiting" ); doInTransaction( new TransactionCallable<Void>() { @Override public Void execute(Session session) { try { Repository repository = (Repository) session.get(Repository. class , 1L); session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(repository); executeNoWait( new Callable<Void>() { @Override public Void call() throws Exception { return doInTransaction( new TransactionCallable<Void>() { @Override public Void execute(Session _session) { LOGGER.info( "Try to get the Repository row" ); startLatch.countDown(); Repository _repository = (Repository) _session.get(Repository. class , 1L); _session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(_repository); Commit _commit = new Commit(_repository); _commit.getChanges().add( new Change( "index.html" , "0a1,2..." )); _session.persist(_commit); _session.flush(); endLatch.countDown(); return null ; } }); } }); startLatch.await(); LOGGER.info( "Sleep for 500ms to delay the other transaction PESSIMISTIC_FORCE_INCREMENT Lock Mode acquisition" ); Thread.sleep( 500 ); Commit commit = new Commit(repository); commit.getChanges().add( new Change( "README.txt" , "0a1,5..." )); commit.getChanges().add( new Change( "web.xml" , "17c17..." )); session.persist(commit); return null ; } catch (InterruptedException e) { fail( "Unexpected failure" ); } return null ; } }); endLatch.await(); } |
Этот тестовый пример генерирует следующий вывод:
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
|
#Alice selects the Repository Query:{[ select lockmodeop0_. id as id1_2_0_, lockmodeop0_.name as name2_2_0_, lockmodeop0_.version as version3_2_0_ from repository lockmodeop0_ where lockmodeop0_. id =?][1]} #Alice locks the Repository using a PESSIMISTIC_FORCE_INCREMENT Lock Mode Query:{[update repository set version=? where id =? and version=?][1,1,0]} #Bob tries to get the Repository but the SELECT is blocked by Alice lock INFO [pool-1-thread-1]: c. v .h.m.l.c.LockModePessimisticForceIncrementTest - Try to get the Repository row #Alice sleeps for 500ms to prove that Bob is waiting for her to release the acquired lock Sleep for 500ms to delay the other transaction PESSIMISTIC_FORCE_INCREMENT Lock Mode acquisition #Alice makes two changes and inserts a new Commit<a href="https://vladmihalcea.files.wordpress.com/2015/02/explicitlockingpessimisticforceincrementfailfast.png"><img src="https://vladmihalcea.files.wordpress.com/2015/02/explicitlockingpessimisticforceincrementfailfast.png?w=585" alt="ExplicitLockingPessimisticForceIncrementFailFast" width="585" height="224" class="alignnone size-large wp-image-3955" /></a> Query:{[insert into commit ( id , repository_id) values (default, ?)][1]} Query:{[insert into commit_change (commit_id, diff , path) values (?, ?, ?)][1,0a1,5...,README.txt] #The Repository version is bumped up to version 1 and a conflict is raised Query:{[insert into commit_change (commit_id, diff , path) values (?, ?, ?)][1,17c17...,web.xml]} Query:{[update repository set version=? where id =? and version=?][1,1,0]} #Alice commits the transaction, therefore releasing all locks DEBUG [main]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection #Bob Repository SELECT can proceed Query:{[ select lockmodepe0_. id as id1_2_0_, lockmodepe0_.name as name2_2_0_, lockmodepe0_.version as version3_2_0_ from repository lockmodepe0_ where lockmodepe0_. id =?][1]} #Bob can insert his changes Query:{[update repository set version=? where id =? and version=?][2,1,1]} Query:{[insert into commit ( id , repository_id) values (default, ?)][1]} Query:{[insert into commit_change (commit_id, diff , path) values (?, ?, ?)][2,0a1,2...,index.html]} |
Этот процесс блокировки можно легко визуализировать на следующей диаграмме:
Реализация двухфазной блокировки базы данных HSQLDB использует блокировки таблицы параметров курса при каждом изменении строки базы данных.
По этой причине Боб не может получить блокировку чтения строки базы данных репозитория, которую Алиса только что обновила. Другие базы данных (например, Oracle, PostgreSQL) используют MVCC , поэтому позволяют SELECT продолжать (используя текущие изменяющиеся журналы отмены транзакции для воссоздания предыдущего состояния строки), в то же время блокируя конфликтующие операторы изменения данных (например, обновляя строку Repository, когда другая параллельная транзакция имеет еще не совершил изменение состояния заблокированного объекта).
Терпеть неудачу быстро
Мгновенное увеличение версии имеет некоторые интересные преимущества:
- Если версия UPDATE успешно выполняется (получена исключительная блокировка на уровне строки), никакая другая параллельная транзакция не может изменить заблокированную строку базы данных. Это момент, когда логическая блокировка (приращение версии) обновляется до физической блокировки (эксклюзивная блокировка базы данных).
- В случае сбоя версии UPDATE (поскольку какая-то другая параллельная транзакция уже зафиксировала изменение версии), текущая текущая транзакция может быть откатана сразу (в отличие от ожидания сбоя транзакции во время фиксации)
Последний вариант использования может быть визуализирован следующим образом:
Для этого сценария мы собираемся использовать следующий тестовый пример:
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
|
@Test public void testConcurrentPessimisticForceIncrementLockingFailFast() throws InterruptedException { LOGGER.info( "Test Concurrent PESSIMISTIC_FORCE_INCREMENT Lock Mode fail fast" ); doInTransaction( new TransactionCallable<Void>() { @Override public Void execute(Session session) { try { Repository repository = (Repository) session.get(Repository. class , 1L); executeAndWait( new Callable<Void>() { @Override public Void call() throws Exception { return doInTransaction( new TransactionCallable<Void>() { @Override public Void execute(Session _session) { Repository _repository = (Repository) _session.get(Repository. class , 1L); _session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(_repository); Commit _commit = new Commit(_repository); _commit.getChanges().add( new Change( "index.html" , "0a1,2..." )); _session.persist(_commit); _session.flush(); return null ; } }); } }); session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_FORCE_INCREMENT)).lock(repository); fail( "Should have thrown StaleObjectStateException!" ); } catch (StaleObjectStateException expected) { LOGGER.info( "Failure: " , expected); } return null ; } }); } |
Генерация следующего вывода:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#Alice selects the Repository Query:{[ select lockmodeop0_. id as id1_2_0_, lockmodeop0_.name as name2_2_0_, lockmodeop0_.version as version3_2_0_ from repository lockmodeop0_ where lockmodeop0_. id =?][1]} #Bob selects the Repository too Query:{[ select lockmodepe0_. id as id1_2_0_, lockmodepe0_.name as name2_2_0_, lockmodepe0_.version as version3_2_0_ from repository lockmodepe0_ where lockmodepe0_. id =?][1]} #Bob locks the Repository using a PESSIMISTIC_FORCE_INCREMENT Lock Mode Query:{[update repository set version=? where id =? and version=?][1,1,0]} #Bob makes a change and inserts a new Commit Query:{[insert into commit ( id , repository_id) values (default, ?)][1]} Query:{[insert into commit_change (commit_id, diff , path) values (?, ?, ?)][1,0a1,2...,index.html]} #Bob commits the transaction DEBUG [pool-3-thread-1]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection #Alice tries to lock the Repository Query:{[update repository set version=? where id =? and version=?][1,1,0]} #Alice cannot lock the Repository, because the version has changed INFO [main]: c. v .h.m.l.c.LockModePessimisticForceIncrementTest - Failure: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.vladmihalcea.hibernate.masterclass.laboratory.concurrency.LockModePessimisticForceIncrementTest$Repository #1] |
Вывод
Как и OPTIMISTIC_FORCE_INCREMENT, режим блокировки PESSIMISTIC_FORCE_INCREMENT полезен для распространения изменения состояния объекта на родительский объект.
Хотя механизм блокировки аналогичен, PESSIMISTIC_FORCE_INCREMENT применяется на месте, что позволяет текущей выполняющейся транзакции мгновенно оценить результат блокировки.
- Код доступен на GitHub .