Вступление
Java Persistence API поставляется с полным механизмом управления параллелизмом, поддерживающим как явную, так и явную блокировку Механизм неявной блокировки прост и основан на:
- Оптимистическая блокировка: изменения состояния объекта могут вызвать увеличение версии
- Блокировка на уровне строк: на основе текущего уровня изоляции выполняемых транзакций операторы INSERT / UPDATE / DELETE могут получить эксклюзивные блокировки строк
Хотя неявная блокировка подходит для многих сценариев, механизм явной блокировки может использовать более детальное управление параллелизмом.
В моих предыдущих постах я рассмотрел явные оптимистические режимы блокировки:
В этом посте я собираюсь раскрыть явные пессимистичные режимы блокировки:
Читатели-писательский замок
Система баз данных — это среда с высокой степенью параллелизма, поэтому многие идиомы теории параллелизма применимы и к доступу к базе данных. Одновременные изменения должны быть сериализованы для сохранения целостности данных, поэтому большинство систем баз данных используют двухфазную стратегию блокировки , даже если она обычно дополняется механизмом управления параллелизмом Multiversion .
Поскольку взаимная исключающая блокировка будет препятствовать масштабируемости (обрабатывать чтение и запись одинаково), большинство систем баз данных используют схему синхронизации блокировки чтения-записи-записи , так что:
- Общая (читаемая) блокировка блокирует писателей, позволяя нескольким читателям продолжить
- Эксклюзивная (запись) блокировка блокирует как читателей, так и писателей, заставляя все операции записи применяться последовательно
Поскольку синтаксис блокировки не является частью стандарта SQL, каждая СУБД выбрала свой синтаксис:
Имя базы данных | Оператор общей блокировки | Эксклюзивная блокировка |
---|---|---|
оракул | ДЛЯ ОБНОВЛЕНИЯ | ДЛЯ ОБНОВЛЕНИЯ |
MySQL | LOCK IN SHARE MODE | ДЛЯ ОБНОВЛЕНИЯ |
Microsoft SQL Server | С (ЗАДЕРЖКА, ТЯГА) | С (UPDLOCK, ROWLOCK) |
PostgreSQL | ДЛЯ ПОДЕЛИТЬСЯ | ДЛЯ ОБНОВЛЕНИЯ |
DB2 | ТОЛЬКО ДЛЯ ЧТЕНИЯ С RS | ДЛЯ ОБНОВЛЕНИЯ С RS |
Уровень абстракции Java Persistence скрывает специфическую семантику блокировки базы данных, предлагая общий API, который требует только двух режимов блокировки. Блокировка совместного использования / чтения получается с использованием типа режима блокировки PESSIMISTIC_READ, и вместо этого запрашивается блокировка эксклюзивного доступа / записи с использованием PESSIMISTIC_WRITE .
Режимы блокировки на уровне строк в PostgreSQL
В следующих тестах мы будем использовать PostgreSQL, поскольку он поддерживает как эксклюзивную, так и разделяемую явную блокировку .
Во всех следующих тестах будет использоваться одна и та же утилита параллелизма, эмулирующая двух пользователей: Алису и Боба. Каждый тестовый сценарий будет проверять определенную комбинацию блокировки чтения / записи.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
private void testPessimisticLocking(ProductLockRequestCallable primaryLockRequestCallable, ProductLockRequestCallable secondaryLockRequestCallable) { doInTransaction(session -> { try { Product product = (Product) session.get(Product. class , 1L); primaryLockRequestCallable.lock(session, product); executeAsync( () -> { doInTransaction(_session -> { Product _product = (Product) _session.get(Product. class , 1L); secondaryLockRequestCallable.lock(_session, _product); }); }, endLatch::countDown ); sleep(WAIT_MILLIS); } catch (StaleObjectStateException e) { LOGGER.info( "Optimistic locking failure: " , e); } }); awaitOnLatch(endLatch); } |
Случай 1: PESSIMISTIC_READ не блокирует запросы блокировки PESSIMISTIC_READ
Первый тест проверит, как взаимодействуют два одновременных запроса блокировки PESSIMISTIC_READ:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
@Test public void testPessimisticReadDoesNotBlockPessimisticRead() throws InterruptedException { LOGGER.info( "Test PESSIMISTIC_READ doesn't block PESSIMISTIC_READ" ); testPessimisticLocking( (session, product) -> { session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_READ)).lock(product); LOGGER.info( "PESSIMISTIC_READ acquired" ); }, (session, product) -> { session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_READ)).lock(product); LOGGER.info( "PESSIMISTIC_READ acquired" ); } ); } |
Запустив этот тест, мы получим следующий вывод:
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
|
[Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - Test PESSIMISTIC_READ doesn't block PESSIMISTIC_READ #Alice selects the Product entity [Alice]: Time:1 Query:{[ SELECT lockmodepe0_. id AS id1_0_0_, lockmodepe0_.description AS descript2_0_0_, lockmodepe0_.price AS price3_0_0_, lockmodepe0_.version AS version4_0_0_ FROM product lockmodepe0_ WHERE lockmodepe0_. id = ? ][1]} #Alice acquires a SHARED lock on the Product entity [Alice]: Time:1 Query:{[ SELECT id FROM product WHERE id =? AND version =? FOR share ][1,0]} [Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - PESSIMISTIC_READ acquired #Alice waits for 500ms [Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - Wait 500 ms! #Bob selects the Product entity [Bob]: Time:1 Query:{[ SELECT lockmodepe0_. id AS id1_0_0_, lockmodepe0_.description AS descript2_0_0_, lockmodepe0_.price AS price3_0_0_, lockmodepe0_.version AS version4_0_0_ FROM product lockmodepe0_ WHERE lockmodepe0_. id = ? ][1]} #Bob acquires a SHARED lock on the Product entity [Bob]: Time:1 Query:{[ SELECT id FROM product WHERE id =? AND version =? FOR share ][1,0]} [Bob]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - PESSIMISTIC_READ acquired #Bob's transactions is committed [Bob]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection #Alice's transactions is committed [Alice]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection |
В этом сценарии нет никаких разногласий. И Алиса, и Боб могут получить общую блокировку, не сталкиваясь с конфликтом.
Случай 2: PESSIMISTIC_READ блокирует UPDATE запросы неявной блокировки
Второй сценарий продемонстрирует, как общая блокировка предотвращает одновременную модификацию. Алиса получит общую блокировку, а Боб попытается изменить заблокированную сущность:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
@Test public void testPessimisticReadBlocksUpdate() throws InterruptedException { LOGGER.info( "Test PESSIMISTIC_READ blocks UPDATE" ); testPessimisticLocking( (session, product) -> { session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_READ)).lock(product); LOGGER.info( "PESSIMISTIC_READ acquired" ); }, (session, product) -> { product.setDescription( "USB Flash Memory Stick" ); session.flush(); LOGGER.info( "Implicit lock acquired" ); } ); } |
Тест генерирует этот вывод:
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
50
|
[Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - Test PESSIMISTIC_READ blocks UPDATE #Alice selects the Product entity [Alice]: Time:0 Query:{[ SELECT lockmodepe0_. id AS id1_0_0_, lockmodepe0_.description AS descript2_0_0_, lockmodepe0_.price AS price3_0_0_, lockmodepe0_.version AS version4_0_0_ FROM product lockmodepe0_ WHERE lockmodepe0_. id = ? ][1]} #Alice acquires a SHARED lock on the Product entity [Alice]: Time:0 Query:{[ SELECT id FROM product WHERE id =? AND version =? FOR share ][1,0]} [Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - PESSIMISTIC_READ acquired #Alice waits for 500ms [Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - Wait 500 ms! #Bob selects the Product entity [Bob]: Time:1 Query:{[ SELECT lockmodepe0_. id AS id1_0_0_, lockmodepe0_.description AS descript2_0_0_, lockmodepe0_.price AS price3_0_0_, lockmodepe0_.version AS version4_0_0_ FROM product lockmodepe0_ WHERE lockmodepe0_. id = ? ][1]} #Alice's transactions is committed [Alice]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection #Bob can acquire the Product entity lock, only after Alice's transaction is committed [Bob]: Time:427 Query:{[ UPDATE product SET description = ?, price = ?, version = ? WHERE id = ? AND version = ? ][USB Flash Memory Stick,12.99,1,1,0]} [Bob]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - Implicit lock acquired #Bob's transactions is committed [Bob]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection |
Хотя Боб мог выбрать сущность Product, UPDATE откладывается до тех пор, пока транзакция Алисы не будет зафиксирована (поэтому для выполнения UPDATE потребовалось 427 мс ).
Случай 3: PESSIMISTIC_READ блокирует запросы блокировки PESSIMISTIC_WRITE
Такое же поведение демонстрируется вторичным запросом блокировки PESSIMISTIC_WRITE:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
@Test public void testPessimisticReadBlocksPessimisticWrite() throws InterruptedException { LOGGER.info( "Test PESSIMISTIC_READ blocks PESSIMISTIC_WRITE" ); testPessimisticLocking( (session, product) -> { session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_READ)).lock(product); LOGGER.info( "PESSIMISTIC_READ acquired" ); }, (session, product) -> { session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(product); LOGGER.info( "PESSIMISTIC_WRITE acquired" ); } ); } |
Дать следующий вывод:
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
|
[Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - Test PESSIMISTIC_READ blocks PESSIMISTIC_WRITE #Alice selects the Product entity [Alice]: Time:0 Query:{[ SELECT lockmodepe0_. id AS id1_0_0_, lockmodepe0_.description AS descript2_0_0_, lockmodepe0_.price AS price3_0_0_, lockmodepe0_.version AS version4_0_0_ FROM product lockmodepe0_ WHERE lockmodepe0_. id = ? ][1]} #Alice acquires a SHARED lock on the Product entity [Alice]: Time:1 Query:{[ SELECT id FROM product WHERE id =? AND version =? FOR share ][1,0]} [Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - PESSIMISTIC_READ acquired #Alice waits for 500ms [Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - Wait 500 ms! #Bob selects the Product entity [Bob]: Time:1 Query:{[ SELECT lockmodepe0_. id AS id1_0_0_, lockmodepe0_.description AS descript2_0_0_, lockmodepe0_.price AS price3_0_0_, lockmodepe0_.version AS version4_0_0_ FROM product lockmodepe0_ WHERE lockmodepe0_. id = ? ][1]} #Alice's transactions is committed [Alice]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection #Bob can acquire the Product entity lock, only after Alice's transaction is committed [Bob]: Time:428 Query:{[ SELECT id FROM product WHERE id = ? AND version = ? FOR UPDATE ][1,0]} [Bob]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - PESSIMISTIC_WRITE acquired #Bob's transactions is committed [Bob]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection |
Запрос эксклюзивной блокировки Боба ждет, пока будет снята общая блокировка Алисы.
Случай 4: PESSIMISTIC_READ блокирует запросы блокировки PESSIMISTIC_WRITE, NO WAIT быстро завершается неудачно
Hibernate предоставляет директиву тайм-аута PESSIMISTIC_NO_WAIT , которая переводит в специфическую для базы данных политику получения блокировки NO_WAIT.
Директива PostgreSQL NO WAIT описывается следующим образом:
Чтобы операция не ожидала принятия других транзакций, используйте параметр NOWAIT. С NOWAIT оператор сообщает об ошибке, а не об ожидании, если выбранная строка не может быть заблокирована немедленно. Обратите внимание, что NOWAIT применяется только к блокировкам на уровне строк — требуемая блокировка на уровне таблиц ROW SHARE по-прежнему выполняется обычным способом (см. Главу 13). Сначала вы можете использовать LOCK с опцией NOWAIT, если вам нужно получить блокировку на уровне таблицы без ожидания.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
@Test public void testPessimisticReadWithPessimisticWriteNoWait() throws InterruptedException { LOGGER.info( "Test PESSIMISTIC_READ blocks PESSIMISTIC_WRITE, NO WAIT fails fast" ); testPessimisticLocking( (session, product) -> { session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_READ)).lock(product); LOGGER.info( "PESSIMISTIC_READ acquired" ); }, (session, product) -> { session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_WRITE)).setTimeOut(Session.LockRequest.PESSIMISTIC_NO_WAIT).lock(product); LOGGER.info( "PESSIMISTIC_WRITE acquired" ); } ); } |
Этот тест генерирует следующий вывод:
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
50
|
[Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - Test PESSIMISTIC_READ blocks PESSIMISTIC_WRITE, NO WAIT fails fast #Alice selects the Product entity [Alice]: Time:1 Query:{[ SELECT lockmodepe0_. id AS id1_0_0_, lockmodepe0_.description AS descript2_0_0_, lockmodepe0_.price AS price3_0_0_, lockmodepe0_.version AS version4_0_0_ FROM product lockmodepe0_ WHERE lockmodepe0_. id = ? ][1]} #Alice acquires a SHARED lock on the Product entity [Alice]: Time:1 Query:{[ SELECT id FROM product WHERE id =? AND version =? FOR share ][1,0]} [Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - PESSIMISTIC_READ acquired #Alice waits for 500ms [Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - Wait 500 ms! #Bob selects the Product entity [Bob]: Time:1 Query:{[ SELECT lockmodepe0_. id AS id1_0_0_, lockmodepe0_.description AS descript2_0_0_, lockmodepe0_.price AS price3_0_0_, lockmodepe0_.version AS version4_0_0_ FROM product lockmodepe0_ WHERE lockmodepe0_. id = ? ][1]} #Bob tries to acquire an EXCLUSIVE lock on the Product entity and fails because of the NO WAIT policy [Bob]: Time:0 Query:{[ SELECT id FROM product WHERE id = ? AND version = ? FOR UPDATE nowait ][1,0]} [Bob]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 0, SQLState: 55P03 [Bob]: o.h.e.j.s.SqlExceptionHelper - ERROR: could not obtain lock on row in relation "product" #Bob's transactions is rolled back [Bob]: o.h.e.t.i.j.JdbcTransaction - rolled JDBC Connection #Alice's transactions is committed [Alice]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection |
Поскольку Алиса уже имеет общую блокировку в строке базы данных, связанной с объектом Product, запрос исключительной блокировки Боба немедленно завершается неудачей.
Случай 5: PESSIMISTIC_WRITE блокирует запросы блокировки PESSIMISTIC_READ
Следующий тест доказывает, что исключительная блокировка всегда блокирует попытку получения общей блокировки:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
@Test public void testPessimisticWriteBlocksPessimisticRead() throws InterruptedException { LOGGER.info( "Test PESSIMISTIC_WRITE blocks PESSIMISTIC_READ" ); testPessimisticLocking( (session, product) -> { session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(product); LOGGER.info( "PESSIMISTIC_WRITE acquired" ); }, (session, product) -> { session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_READ)).lock(product); LOGGER.info( "PESSIMISTIC_WRITE acquired" ); } ); } |
Генерация следующего вывода:
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
|
[Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - Test PESSIMISTIC_WRITE blocks PESSIMISTIC_READ #Alice selects the Product entity [Alice]: Time:1 Query:{[ SELECT lockmodepe0_. id AS id1_0_0_, lockmodepe0_.description AS descript2_0_0_, lockmodepe0_.price AS price3_0_0_, lockmodepe0_.version AS version4_0_0_ FROM product lockmodepe0_ WHERE lockmodepe0_. id = ? ][1]} #Alice acquires an EXCLUSIVE lock on the Product entity [Alice]: Time:0 Query:{[ SELECT id FROM product WHERE id = ? AND version = ? FOR UPDATE ][1,0]} [Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - PESSIMISTIC_WRITE acquired #Alice waits for 500ms [Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - Wait 500 ms! #Bob selects the Product entity [Bob]: Time:1 Query:{[ SELECT lockmodepe0_. id AS id1_0_0_, lockmodepe0_.description AS descript2_0_0_, lockmodepe0_.price AS price3_0_0_, lockmodepe0_.version AS version4_0_0_ FROM product lockmodepe0_ WHERE lockmodepe0_. id = ? ][1]} #Alice's transactions is committed [Alice]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection #Bob can acquire the Product entity SHARED lock, only after Alice's transaction is committed [Bob]: Time:428 Query:{[ SELECT id FROM product WHERE id =? AND version =? FOR share ][1,0]} [Bob]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - PESSIMISTIC_WRITE acquired #Bob's transactions is committed [Bob]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection |
Запрос общей блокировки Боба ожидает завершения транзакции Алисы, поэтому все полученные блокировки снимаются.
Случай 6: PESSIMISTIC_WRITE блокирует запросы блокировки PESSIMISTIC_WRITE
Эксклюзивный замок также блокирует эксклюзивный замок:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
@Test public void testPessimisticWriteBlocksPessimisticWrite() throws InterruptedException { LOGGER.info( "Test PESSIMISTIC_WRITE blocks PESSIMISTIC_WRITE" ); testPessimisticLocking( (session, product) -> { session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(product); LOGGER.info( "PESSIMISTIC_WRITE acquired" ); }, (session, product) -> { session.buildLockRequest( new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(product); LOGGER.info( "PESSIMISTIC_WRITE acquired" ); } ); } |
Тест генерирует этот вывод:
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
|
[Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - Test PESSIMISTIC_WRITE blocks PESSIMISTIC_WRITE #Alice selects the Product entity [Alice]: Time:1 Query:{[ SELECT lockmodepe0_. id AS id1_0_0_, lockmodepe0_.description AS descript2_0_0_, lockmodepe0_.price AS price3_0_0_, lockmodepe0_.version AS version4_0_0_ FROM product lockmodepe0_ WHERE lockmodepe0_. id = ? ][1]} #Alice acquires an EXCLUSIVE lock on the Product entity [Alice]: Time:0 Query:{[ SELECT id FROM product WHERE id = ? AND version = ? FOR UPDATE ][1,0]} [Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - PESSIMISTIC_WRITE acquired #Alice waits for 500ms [Alice]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - Wait 500 ms! #Bob selects the Product entity [Bob]: Time:1 Query:{[ SELECT lockmodepe0_. id AS id1_0_0_, lockmodepe0_.description AS descript2_0_0_, lockmodepe0_.price AS price3_0_0_, lockmodepe0_.version AS version4_0_0_ FROM product lockmodepe0_ WHERE lockmodepe0_. id = ? ][1]} #Alice's transactions is committed [Alice]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection #Bob can acquire the Product entity SHARED lock, only after Alice's transaction is committed [Bob]: Time:428 Query:{[ SELECT id FROM product WHERE id =? AND version =? FOR update ][1,0]} [Bob]: c. v .h.m.l.c.LockModePessimisticReadWriteIntegrationTest - PESSIMISTIC_WRITE acquired #Bob's transactions is committed [Bob]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection |
Запрос эксклюзивной блокировки Боба должен подождать, пока Алиса снимет блокировку.
Вывод
Реляционные системы баз данных используют блокировки для сохранения гарантий ACID , поэтому важно понимать, как взаимодействуют общие и эксклюзивные блокировки на уровне строк. Явная пессимистическая блокировка — это очень мощный механизм управления параллелизмом базы данных, и вы можете даже использовать его для исправления состояния гонки оптимистичной блокировки .
- Код доступен на GitHub .
Ссылка: | Шаблоны блокировки гибернации — как PESSIMISTIC_READ и PESSIMISTIC_WRITE работают от нашего партнера по JCG Влада Михалча в блоге Влада Михалчеа . |