Вступление
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
|
@Testpublic 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 idFROM productWHERE 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 idFROM productWHERE 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
|
@Testpublic 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 idFROM productWHERE 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 productSET 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
|
@Testpublic 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 idFROM productWHERE 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 idFROM productWHERE 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
|
@Testpublic 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 idFROM productWHERE 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 idFROM productWHERE 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
|
@Testpublic 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 idFROM productWHERE 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 idFROM productWHERE 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
|
@Testpublic 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 idFROM productWHERE 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 idFROM productWHERE 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 Влада Михалча в блоге Влада Михалчеа . |