# Занятие 05: Архитектура / Блокировки

> Исходная страница в нативном Markdown: Архитектура / Блокировки

---

Индекс LLMS: [llms.txt](/ru/llms.txt)

---

<!-- Generated by scripts/generate_skeleton.py. -->

> [!NOTE]
> Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета.
> **HTML source:** [dev2_05_arch_locks.html](https://edu.postgrespro.ru/16/dev2-16/dev2_05_arch_locks.html)
> **Archive:** [DEV2-handouts-16.zip](https://edu.postgrespro.ru/16/DEV2-handouts-16.zip)
> **Страниц PDF:** 30

## Page 1: Архитектура / Блокировки {#source-page-001}

### Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
### Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.
### Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru
### Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

## Page 2: Темы {#source-page-002}

- Общая информация о блокировках
- Блокировки отношений и других объектов
- Блокировки на уровне строк

## Page 3: Блокировки {#source-page-003}

**Задача: упорядочение конкурентного доступа к разделяемым ресурсам**
- Механизм
  - перед обращением к данным процесс захватывает блокировку
  - после обращения — освобождает (обычно в конце транзакции)
  - несовместимые блокировки приводят к очередям

### Комментарии преподавателя

Блокировки используются, чтобы упорядочить конкурентный доступ к разделяемым ресурсам.

Под конкурентным доступом понимается одновременный доступ нескольких процессов. Сами процессы могут выполняться как параллельно (если позволяет аппаратура), так и последовательно в режиме разделения времени.

Блокировки не нужны, если нет конкуренции (одновременно к данным обращается только один процесс) или если нет разделяемого ресурса (например, общий буферный кеш нуждается в блокировках, а локальный — нет).

Перед тем, как обратиться к ресурсу, защищенному блокировкой, процесс должен захватить эту блокировку. Когда процесс перестает нуждаться в ресурсе, он освобождает блокировку, чтобы ресурсом могли воспользоваться другие процессы.

Захват блокировки возможен не всегда: ресурс может оказаться уже занятым кем-то другим в несовместимом режиме. В простом случае используют два режима: исключительный (несовместим ни с чем) и разделяемый (совместим сам с собой). Но их может быть и больше, в этом случае совместимость определяется матрицей.

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

## Page 4: Блокировки объектов {#source-page-004}

**Блокировки номера транзакции**
- Блокировки отношений
- Очередь ожидания

### Комментарии преподавателя

Из всего многообразия блокировок на уровне объектов мы рассмотрим только блокировки номера транзакции и блокировки отношений.

Еще один вид блокировок этого же типа — рекомендательные блокировки — упоминался в теме «Обработка ошибок» курса DEV1.

## Page 5: Номер транзакции {#source-page-005}

**Типы ресурсов в pg\\\_locks**
  - virtualxid — виртуальный номер транзакции
  - transactionid — настоящий номер транзакции
- Режимы
  - исключительный
  - разделяемый
- Транзакция удерживает исключительную блокировку собственного номера
  - способ дождаться завершения транзакции

### Комментарии преподавателя

Рассмотрение блокировок на уровне объектов («обычные», «тяжелые» блокировки) мы начнем с блокировок номера транзакции.

Блокировки объектов хранятся в общей памяти сервера, поэтому их количество ограничено. Все такие блокировки можно посмотреть в представлении pg\_locks: они устроены одинаково и отличаются только типами ресурсов и режимами блокирования. Номерам транзакций соответствуют типы ресурсов Transactionid и Virtualxid.

Каждой транзакции при старте назначается сначала виртуальный номер, который используется, пока транзакция только читает данные. Это сделано для экономии настоящих номеров — виртуальный номер никак не учитывается в правилах видимости. Его можно выдать быстро, без обращения к общей памяти (требуется лишь уникальность номеров всех активных транзакций).

Уникальный настоящий номер выдается счетчиком XID в тот момент, когда транзакция изменяет какие-либо данные. Он нужен, чтобы отслеживать статус пишущей транзакции (активна, зафиксирована или оборвана); такой номер можно записывать в поля xmin и xmax заголовка версий строк.

Каждая транзакция удерживает исключительную блокировку своих номеров: и виртуального, и настоящего, если он есть. Это дает простой способ дождаться окончания какой-либо транзакции: надо запросить блокировку ее номера. Если блокировку сразу получить не удастся, то запрашивающий процесс заснет и будет разбужен только по завершении транзакции — когда блокировка освободится.

## Page 6: Блокировка номера транзакции {#source-page-006}

![Source slide page 6: Блокировка номера транзакции](images/page-006.png){#source-figure-006 caption="Source slide page 6: Блокировка номера транзакции"}

```sql
=> CREATE DATABASE arch_locks;
```
```sql
CREATE DATABASE
```
```sql
=> \c arch_locks
```
```text
You are now connected to database "arch_locks" as user "student".
```
Начнем в другом сеансе новую транзакцию.
```sql
=> \c arch_locks
```
```text
You are now connected to database "arch_locks" as user "student".
```
```text
=> BEGIN;
```
```text
BEGIN
```
Нам понадобится номер обслуживающего процесса:
```sql
=> SELECT pg_backend_pid();
```
```text
 pg_backend_pid
----------------
          10981
(1 row)
```
Все «обычные» блокировки видны в представлении pg\_locks. Какие блокировки удерживает только что начатая транзакция?
```sql
=> SELECT locktype, virtualxid AS virtxid, transactionid AS xid,
    mode, granted
FROM pg_locks
WHERE pid = 10981;
```
```text
  locktype  | virtxid | xid |     mode      | granted
------------+---------+-----+---------------+---------
 virtualxid | 3/12    |     | ExclusiveLock | t
(1 row)
```
locktype — тип ресурса, mode — режим блокировки, granted — удалось ли получить блокировку.
Каждой транзакции сразу выдается виртуальный номер, и транзакция удерживает его исключительную блокировку.
Как только транзакция начинает менять какие-либо данные, ей выдается настоящий номер, который учитывается в правилах видимости. Номер можно получить и явно:
```sql
=> SELECT pg_current_xact_id();
```
```text
 pg_current_xact_id
--------------------
                816
(1 row)
```
```sql
=> SELECT locktype, virtualxid AS virtxid, transactionid AS xid,
    mode, granted
FROM pg_locks
WHERE pid = 10981;
```
```text
   locktype    | virtxid | xid |     mode      | granted
---------------+---------+-----+---------------+---------
 virtualxid    | 3/12    |     | ExclusiveLock | t
 transactionid |         | 816 | ExclusiveLock | t
(2 rows)
```
Теперь транзакция удерживает исключительную блокировку обоих номеров.

## Page 7: Блокировки отношений {#source-page-007}

**Тип ресурса в pg\\\_locks**
  - relation — таблицы, индексы и т. п.
- Режимы
  - Access Share
  - SELECT
  - Row Share
  - SELECT FOR UPDATE/SHARE
  - Row Exclusive
  - UPDATE, DELETE, INSERT, MERGE
  - Share Update Exclusive
  - VACUUM, ALTER TABLE,
  - CREATE INDEX CONCURRENTLY
  - Share
  - CREATE INDEX
  - Share Row Exclusive
  - CREATE TRIGGER, ALTER TABLE
  - Exclusive
  - REFRESH MAT. VIEW CONCURRENTLY
  - Access Exclusive
  - DROP, TRUNCATE, REINDEX, VACUUM FULL,
  - LOCK TABLE, ALTER TABLE, REFRESH MAT. VIEW
  - допускают
  - параллельное
  - изменение
  - данных
  - в таблице

### Комментарии преподавателя

Второй важный случай блокировок объектов — блокировки отношений (таблиц, индексов, последовательностей и т. п.). Такие блокировки имеют тип relation в pg\_locks.

Для них определено целых 8 различных режимов, которые показаны на слайде вместе с примерами команд SQL, использующих эти режимы. Матрица совместимости здесь не показана из-за ее большого размера, но она приведена в документации:

<https://postgrespro.ru/docs/postgresql/16/explicit-locking#LOCKING-TABLES>

Такое количество режимов существует для того, чтобы позволить выполнять одновременно как можно большее количество команд, относящихся к одной таблице (индексу и т. п.).

Самый слабый режим — Access Share, он захватывается командой SELECT и совместим с любым режимом, кроме самого сильного — Access Exclusive. Это означает, что запрос не мешает ни другим запросам, ни изменению данных в таблице, ни чему-либо другому, но не дает, например, удалить таблицу в то время, как из нее читаются данные.

Другой пример: режим Share (как и другие более сильные режимы) не совместим с изменением данных в таблице. Например, команда CREATE INDEX заблокирует команды INSERT, UPDATE, DELETE и MERGE (и наоборот). Поэтому существует команда CREATE INDEX CONCURRENTLY, использующая режим Share Update Exclusive, который совместим с такими изменениями (за счет этого команда выполняется дольше).

## Page 8: Очередь ожидания {#source-page-008}

![Source slide page 8: Очередь ожидания](images/page-008.png){#source-figure-008 caption="Source slide page 8: Очередь ожидания"}


### Комментарии преподавателя

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

Пусть, как на рисунке, транзакция T100 выполняет команду UPDATE, при этом на отношение будет установлена блокировка с режимом Row Exclusive.

Транзакция T110, выполняющая CREATE INDEX, встает в очередь за T100, поскольку запрашивает режим Share, не совместимый с Row Exclusive. Если следом придет транзакция T120 с командой VACUUM FULL (ей нужен режим Access Exclusive, не совместимый ни с каким другим), она встанет в очередь за CREATE INDEX.

А еще одна транзакция с обычным запросом SELECT (требует Access Share, совместимый с выполняющейся сейчас командой UPDATE) тоже честно займет очередь за VACUUM FULL.

## Page 9: Очередь ожидания {#source-page-009}

![Source slide page 9: Очередь ожидания](images/page-009.png){#source-figure-009 caption="Source slide page 9: Очередь ожидания"}


### Комментарии преподавателя

После того как первая транзакция завершается, блокировку захватывает следующая транзакция, стоящая в очереди.

## Page 10: Блокировка отношений {#source-page-010}

Создадим таблицу банковских счетов с тремя строками:
```sql
=> CREATE TABLE accounts(acc_no integer, amount numeric);
```
```sql
CREATE TABLE
```
```sql
=> INSERT INTO accounts VALUES (1,100.00), (2,200.00), (3,300.00);
```
```sql
INSERT 0 3
```
Вторая транзакция, которую мы не завершали, продолжает работу. Выполним в ней запрос к таблице:
```sql
=> SELECT * FROM accounts;
```
```text
 acc_no | amount
--------+--------
      1 | 100.00
      2 | 200.00
      3 | 300.00
(3 rows)
```
Как изменятся блокировки?
```sql
=> SELECT locktype, relation::regclass, virtualxid AS virtxid,
    transactionid AS xid, mode, granted
FROM pg_locks
WHERE pid = 10981;
```
```text
   locktype    | relation | virtxid | xid |      mode       | granted
---------------+----------+---------+-----+-----------------+---------
 relation      | accounts |         |     | AccessShareLock | t
 virtualxid    |          | 3/12    |     | ExclusiveLock   | t
 transactionid |          |         | 816 | ExclusiveLock   | t
(3 rows)
```
Добавилась блокировка таблицы в режиме Access Share. Она совместима с блокировкой любого режима, кроме Access Exclusive, поэтому не мешает практически никаким операциям, но не дает, например, удалить таблицу. Попробуем.
```sql
=> \c arch_locks
```
```text
You are now connected to database "arch_locks" as user "student".
```
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> SELECT pg_backend_pid();
```
```text
 pg_backend_pid
----------------
          11421
(1 row)
```
```sql
=> DROP TABLE accounts;
```
Команда не выполняется — ждет освобождения блокировки. Какой?
```sql
=> SELECT locktype, relation::regclass, virtualxid AS virtxid,
    transactionid AS xid, mode, granted
FROM pg_locks
WHERE pid = 11421;
```
```text
   locktype    | relation | virtxid | xid |        mode         | granted
---------------+----------+---------+-----+---------------------+---------
 virtualxid    |          | 4/5     |     | ExclusiveLock       | t
 relation      | accounts |         |     | AccessExclusiveLock | f
 transactionid |          |         | 819 | ExclusiveLock       | t
(3 rows)
```
Транзакция пыталась получить блокировку таблицы в режиме Access Exclusive, но не смогла (granted = f).
Информацию о том, что транзакция ожидает чего-то для продолжения работы, можно получить и так:

## Page 11: Page 11 {#source-page-011}

```sql
=> SELECT state, wait_event_type, wait_event
FROM pg_stat_activity
WHERE pid = 11421;
```
```text
 state  | wait_event_type | wait_event
--------+-----------------+------------
 active | Lock            | relation
(1 row)
```
state — состояние: активная транзакция, выполняющая команду; wait\_event\_type — тип ожидания: блокировка (бывают и другие, например, ожидание ввода-вывода); wait\_event — конкретное ожидание: ожидание блокировки отношения.
Мы можем найти номер блокирующего процесса (в общем случае — несколько номеров)...
```sql
=> SELECT pg_blocking_pids(11421);
```
```text
 pg_blocking_pids
------------------
 {10981}
(1 row)
```
...и посмотреть информацию о сеансах, к которым они относятся:
```sql
=> SELECT * FROM pg_stat_activity
WHERE pid = ANY(pg_blocking_pids(11421)) \gx
```
```sql
-[ RECORD 1 ]----+------------------------------
datid            | 16566
datname          | arch_locks
pid              | 10981
leader_pid       |
usesysid         | 16384
usename          | student
application_name | psql
client_addr      |
client_hostname  |
client_port      | -1
backend_start    | 2026-04-10 16:02:24.978483+03
xact_start       | 2026-04-10 16:02:25.07702+03
query_start      | 2026-04-10 16:02:25.958195+03
state_change     | 2026-04-10 16:02:25.963636+03
wait_event_type  | Client
wait_event       | ClientRead
state            | idle in transaction
backend_xid      | 816
backend_xmin     |
query_id         |
query            | SELECT * FROM accounts;
backend_type     | client backend
```
После завершения транзакции все блокировки снимаются и таблица удаляется:
```text
=> COMMIT;
```
```text
COMMIT
```
```sql
DROP TABLE
```
```text
=> COMMIT;
```
```text
COMMIT
```

## Page 12: Блокировки строк {#source-page-012}

**Разделяемые и исключительные блокировки**
- «Нечестная» очередь ожидания

## Page 13: Блокировки строк {#source-page-013}

**Используются совместно с многоверсионностью, для чтения данных не требуются**
- Информация только в страницах данных
  - в оперативной памяти ничего не хранится
- Неограниченное количество
  - большое число не влияет на производительность
- Инфраструктура
  - очередь ожидания организована с помощью блокировок объектов

### Комментарии преподавателя

Благодаря многоверсионности, блокировки уровня строк нужны только при изменении данных (или для того, чтобы не допустить изменения) и не нужны при обычном чтении.

Если в случае блокировок объектов каждый ресурс представлен собственной блокировкой в оперативной памяти, то со строками так не получается: отдельная блокировка для каждой табличной строки (которых могут быть миллионы и миллиарды) потребует непомерных накладных расходов и огромного объема оперативной памяти.

Один из вариантов решения — повышение уровня (эскалация): если уже заблокировано N строк таблицы, то при блокировке еще одной блокируется вся таблица целиком, а блокировки на уровне строк снимаются. Но в этом случае страдает пропускная способность.

Поэтому в PostgreSQL сделано иначе. Информация о том, что строка заблокирована, хранится исключительно в заголовке версии строки внутри страницы данных. Там она представлена номером блокирующей транзакции (xmax) и дополнительными информационными битами.

За счет этого можно установить неограниченное количество блокировок уровня строки, и это не требует дополнительных ресурсов и не снижает производительность системы.

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

## Page 14: Блокировки строк {#source-page-014}

![Source slide page 14: Блокировки строк](images/page-014.png){#source-figure-014 caption="Source slide page 14: Блокировки строк"}

**Режимы**
  - Update — удаление строки или изменение всех полей
  - No Key Update — изменение любых полей, кроме ключевых
  - Share — запрет изменения любых полей строки
  - Key Share — запрет изменения ключевых полей строки
  - Key Share
  - Share
  - No Key Update
  - Update

### Комментарии преподавателя

Существует 4 режима, в которых можно заблокировать строку (в версии строки режим указывается с помощью дополнительных информационных битов).

Два режима представляют исключительные (exclusive) блокировки, которые одновременно может удерживать только одна транзакция. Режим UPDATE предполагает полное изменение (или удаление) строки, а режим NO KEY UPDATE — изменение только тех полей, которые не входят в уникальные индексы (иными словами, при таком изменении все внешние ключи остаются без изменений). Команда UPDATE сама выбирает минимальный подходящий режим блокировки; обычно строки блокируются в режиме NO KEY UPDATE.

Еще два режима представляют разделяемые (shared) блокировки, которые могут удерживаться несколькими транзакциями.

Режим SHARE применяется, когда нужно прочитать строку, но при этом нельзя допустить, чтобы она как-либо изменилась другой транзакцией. Режим KEY SHARE допускает изменение строки, но только неключевых полей. Этот режим, в частности, автоматически используется самим PostgreSQL при проверке внешних ключей.

Напомним, что обычное чтение (SELECT) вообще не блокирует строки.

Матрица совместимости режимов приведена внизу слайда. Из нее видно, что разделяемый режим KEY SHARE совместим с исключительным режимом NO KEY UPDATE (то есть можно обновлять неключевые поля и быть уверенным в том, что ключ не изменится).

<https://postgrespro.ru/docs/postgresql/16/explicit-locking#LOCKING-ROWS>

## Page 15: Блокировка строк {#source-page-015}

![Source slide page 15: Блокировка строк](images/page-015.png){#source-figure-015 caption="Source slide page 15: Блокировка строк"}

Снова создадим таблицу счетов, но теперь сделаем номер счета первичным ключом:
```sql
=> CREATE TABLE accounts(acc_no integer PRIMARY KEY, amount numeric);
```
```sql
CREATE TABLE
```
```sql
=> INSERT INTO accounts VALUES (1,100.00), (2,200.00), (3,300.00);
```
```sql
INSERT 0 3
```
В новой транзакции обновим сумму первого счета (при этом ключ не меняется):
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;
```
```sql
UPDATE 1
```
```sql
=> SELECT pg_current_xact_id();
```
```text
 pg_current_xact_id
--------------------
                822
(1 row)
```
Как правило, признаком блокировки строки служит номер блокирующей транзакции, записанный в поле xmax (и еще ряд информационных битов, определяющих режим блокировки):
```sql
=> SELECT xmax, * FROM accounts;
```
```text
 xmax | acc_no | amount
------+--------+--------
  822 |      1 | 100.00
    0 |      2 | 200.00
    0 |      3 | 300.00
(3 rows)
```
Но в случае разделяемых блокировок такой способ не годится, поскольку в xmax нельзя записать несколько номеров транзакций. В таком случае номера транзакций хранятся в отдельной структуре, называемой мультитранзакцией, а в xmax помещается ссылка на нее. Но мы не будем вдаваться в детали реализации блокировок строк, а просто воспользуемся расширением pgrowlocks:
```sql
=> CREATE EXTENSION pgrowlocks;
```
```sql
CREATE EXTENSION
```
```sql
=> SELECT locked_row, xids, modes, pids FROM pgrowlocks('accounts');
```
```sql
 locked_row | xids  |       modes       |  pids
------------+-------+-------------------+---------
 (0,1)      | {822} | {"No Key Update"} | {10981}
(1 row)
```
Чтобы показать блокировки, расширение читает табличные страницы (в отличие от обращения к pg\_locks, которое читает данные из оперативной памяти).
Теперь изменим номер второго счета (при этом меняется ключ):
```sql
=> UPDATE accounts SET acc_no = 20 WHERE acc_no = 2;
```
```sql
UPDATE 1
```
```sql
=> SELECT locked_row, xids, modes, pids FROM pgrowlocks('accounts');
```
```sql
 locked_row | xids  |       modes       |  pids
------------+-------+-------------------+---------
 (0,1)      | {822} | {"No Key Update"} | {10981}
 (0,2)      | {822} | {Update}          | {10981}
(2 rows)
```
Чтобы продемонстрировать разделяемые блокировки, начнем еще одну транзакцию. Все запрашиваемые блокировки будут совместимы друг с другом.

## Page 16: Page 16 {#source-page-016}

![Source slide page 16: Page 16](images/page-016.png){#source-figure-016 caption="Source slide page 16: Page 16"}

```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> SELECT * FROM accounts WHERE acc_no = 1 FOR KEY SHARE;
```
```text
 acc_no | amount
--------+--------
      1 | 100.00
(1 row)
```
```sql
=> SELECT * FROM accounts WHERE acc_no = 3 FOR SHARE;
```
```text
 acc_no | amount
--------+--------
      3 | 300.00
(1 row)
```
```sql
=> SELECT pg_current_xact_id();
```
```text
 pg_current_xact_id
--------------------
                824
(1 row)
```
```sql
=> SELECT locked_row, xids, modes, pids FROM pgrowlocks('accounts');
```
```sql
 locked_row |   xids    |             modes             |     pids
------------+-----------+-------------------------------+---------------
 (0,1)      | {822,824} | {"No Key Update","Key Share"} | {10981,11421}
 (0,2)      | {822}     | {Update}                      | {10981}
 (0,3)      | {824}     | {"For Share"}                 | {11421}
(3 rows)
```
```text
=> ROLLBACK;
```
```text
ROLLBACK
```
```text
=> ROLLBACK;
```
```text
ROLLBACK
```
### Как не ждать блокировку?
Иногда удобно не ждать освобождения блокировки, а сразу получить ошибку, если необходимый ресурс занят. Приложение может перехватить и обработать такую ошибку.
Для этого ряд команд SQL (такие, как SELECT и некоторые варианты ALTER) позволяют указать ключевое слово NOWAIT. Заблокируем таблицу, обновив первую строку:
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> UPDATE accounts SET amount = amount + 1 WHERE acc_no = 1;
```
```sql
UPDATE 1
```
```text
=> BEGIN;
```
```text
BEGIN
```
```text
=> LOCK TABLE accounts NOWAIT; -- IN ACCESS EXCLUSIVE MODE
```
```text
ERROR:  could not obtain lock on relation "accounts"
```
Транзакция сразу же получает ошибку.
```text
=> ROLLBACK;
```
```text
ROLLBACK
```
Команды UPDATE и DELETE не позволяют указать NOWAIT. Но можно сначала выполнить команду
```sql
SELECT ... FOR UPDATE NOWAIT; -- или FOR NO KEY UPDATE NOWAIT
```
а затем, если строки успешно заблокированы, изменить или удалить их. Например:
```text
=> BEGIN;
```

## Page 17: Page 17 {#source-page-017}

![Source slide page 17: Page 17](images/page-017.png){#source-figure-017 caption="Source slide page 17: Page 17"}

```text
BEGIN
```
```sql
=> SELECT * FROM accounts WHERE acc_no = 1 FOR UPDATE NOWAIT;
```
```text
ERROR:  could not obtain lock on row in relation "accounts"
```
Снова тут же получаем ошибку - строка уже заблокирована. Но при успехе SELECT ... FOR UPDATE в этой транзакции можно было бы далее изменять заблокированную строку.
```text
=> ROLLBACK;
```
```text
ROLLBACK
```
Другой подход к блокировке строк предоставляет предложение SKIP LOCKED. Запросим блокировку одной строки, не указывая конкретный номер счета:
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> SELECT * FROM accounts ORDER BY acc_no
FOR UPDATE SKIP LOCKED LIMIT 1;
```
```text
 acc_no | amount
--------+--------
      2 | 200.00
(1 row)
```
В этом случае команда пропускает уже заблокированную первую строку и мы немедленно получаем блокировку второй строки. Этот прием уже использовался в практике темы «Очистка» при выборе пакета строк для обновления. Еще одно применение — в организации очередей — будет рассмотрено в теме «Асинхронная обработка».
```text
=> ROLLBACK;
```
```text
ROLLBACK
```
Для команд, не связанных с блокировкой строк, использовать NOWAIT не получится. В этом случае можно установить небольшой таймаут ожидания (по умолчанию он не задан и ожидание будет бесконечным):
```text
=> SET lock_timeout = '1s';
```
```text
SET
```
```sql
=> ALTER TABLE accounts DROP COLUMN amount;
```
```text
ERROR:  canceling statement due to lock timeout
```
Получаем ошибку без длительного ожидания освобождения ресурса.
```text
=> RESET lock_timeout;
```
```text
RESET
```
```text
=> ROLLBACK;
```
```text
ROLLBACK
```

## Page 18: «Очередь» ожидания {#source-page-018}

![Source slide page 18: «Очередь» ожидания](images/page-018.png){#source-figure-018 caption="Source slide page 18: «Очередь» ожидания"}


### Комментарии преподавателя

Для эффективной организации очереди ожидания используется механизм блокировок объектов, при этом информация хранится в оперативной памяти и видна в pg\_locks.

Допустим, транзакции с номером 101 удалось заблокировать строку в разделяемом режиме Share. Информация о блокировке записывается в заголовок версии строки — в поле xmax появляется номер этой транзакции.

## Page 19: «Очередь» ожидания {#source-page-019}

![Source slide page 19: «Очередь» ожидания](images/page-019.png){#source-figure-019 caption="Source slide page 19: «Очередь» ожидания"}


### Комментарии преподавателя

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

Поскольку каждая транзакция удерживает исключительную блокировку своего номера, транзакция 102 запрашивает блокировку номера транзакции 101, чтобы дождаться ее завершения.

Кроме того, ожидающая транзакция удерживает блокировку условного ресурса типа tuple (версия строки), показывая, что она первая в «очереди».

## Page 20: «Очередь» ожидания {#source-page-020}

![Source slide page 20: «Очередь» ожидания](images/page-020.png){#source-figure-020 caption="Source slide page 20: «Очередь» ожидания"}


### Комментарии преподавателя

Если появляются другие транзакции, конфликтующие с текущей блокировкой версии строки (в нашем примере — 112, 122, 132), первым делом они пытаются захватить блокировку типа tuple для этой версии.

Поскольку блокировка tuple уже удерживается транзакцией 102, другие транзакции ждут освобождения этой блокировки. Получается своеобразная «очередь», в которой есть первый и все остальные.

Если бы все прибывающие транзакции занимали очередь непосредственно за транзакцией 101, при освобождении блокировки возникала бы ситуация гонки. При неудачном стечении обстоятельств транзакция 102 могла бы ждать блокировку вечно. Двухуровневая схема блокирования немного упорядочивает подобную конкуренцию.

Стоит избегать проектных решений, которые предполагают массовые изменения одной и той же строки. В этом случае возникает «горячая точка», которая на высоких нагрузках может привести к снижению производительности.

## Page 21: «Очередь» ожидания {#source-page-021}

![Source slide page 21: «Очередь» ожидания](images/page-021.png){#source-figure-021 caption="Source slide page 21: «Очередь» ожидания"}


### Комментарии преподавателя

В нашем примере транзакция 101 заблокировала строку в разделяемом (Share) режиме. Пока транзакция 102 ожидает завершения транзакции 101, может появиться еще транзакция 133, желающая получить блокировку строки в разделяемом режиме, совместимом с текущей блокировкой транзакции 101.

Такая транзакция не стоит в очереди, она сразу получает блокировку. В этом случае список номеров блокирующих транзакций будет вынесен в мультитранзакцию, а в поле xmax строки запишется номер этой мультитранзакции.

## Page 22: «Очередь» ожидания {#source-page-022}

![Source slide page 22: «Очередь» ожидания](images/page-022.png){#source-figure-022 caption="Source slide page 22: «Очередь» ожидания"}


### Комментарии преподавателя

Когда транзакция 101 завершится, транзакция 102 будет разбужена, но не сможет захватить блокировку строки, поскольку строка все еще заблокирована транзакцией 133. Транзакция 102 будет вынуждена снова «заснуть».

PostgreSQL использует разделяемые блокировки для внутренних нужд, в частности, при проверке внешних ключей. В прикладном коде можно явно запросить разделяемую блокировку с помощью команды SELECT ... FOR SHARE или SELECT ... FOR KEY SHARE.

Однако следует помнить, что при постоянном потоке разделяемых блокировок пишущая транзакция в худшем случае может ждать своей очереди бесконечно. Эта ситуация называется по-английски locker starvation.

Заметим, что такой проблемы в принципе не возникает при блокировках объектов (таких, как отношения). В этом случае каждый ресурс представлен собственной блокировкой в оперативной памяти и все ждущие процессы выстраиваются в «честную» очередь.

<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/acc> ess/heap/README.tuplock;hb=HEAD

## Page 23: «Очередь» ожидания {#source-page-023}

![Source slide page 23: «Очередь» ожидания](images/page-023.png){#source-figure-023 caption="Source slide page 23: «Очередь» ожидания"}


### Комментарии преподавателя

Когда транзакция 133 завершится, транзакция 102 получит возможность первой записать свой номер в поле xmax, после чего она освободит блокировку tuple. Тогда одна случайная транзакция из всех остальных успеет захватить блокировку tuple и станет первой в «очереди».

## Page 24: «Очередь» ожидания {#source-page-024}

Начинаем транзакцию и обновляем строку.
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;
```
```sql
UPDATE 1
```
Будем смотреть только на блокировки, связанные с номерами транзакций и версиями строк:
```sql
=> SELECT pid, locktype, page, tuple, transactionid AS xid,
    mode, granted
FROM pg_locks WHERE locktype IN ('transactionid','tuple')
ORDER BY pid, granted DESC, locktype;
```
```text
  pid  |   locktype    | page | tuple | xid |     mode      | granted
-------+---------------+------+-------+-----+---------------+---------
 10935 | transactionid |      |       | 829 | ExclusiveLock | t
(1 row)
```
Другая транзакция пытается обновить ту же строку:
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;
```
Какие при этом возникают блокировки?
```sql
=> SELECT pid, locktype, page, tuple, transactionid AS xid,
    mode, granted
FROM pg_locks WHERE locktype IN ('transactionid','tuple')
ORDER BY pid, granted DESC, locktype;
```
```text
  pid  |   locktype    | page | tuple | xid |     mode      | granted
-------+---------------+------+-------+-----+---------------+---------
 10935 | transactionid |      |       | 829 | ExclusiveLock | t
 10981 | transactionid |      |       | 830 | ExclusiveLock | t
 10981 | tuple         |    0 |     1 |     | ExclusiveLock | t
 10981 | transactionid |      |       | 829 | ShareLock     | f
(4 rows)
```
Транзакция захватила блокировку, связанную с версией строки 1 на странице 0, и ждет завершения первой транзакции:
```sql
=> SELECT pid, pg_blocking_pids(pid) FROM pg_stat_activity
WHERE pid IN (10935,10981) ORDER BY pid;
```
```text
  pid  | pg_blocking_pids
-------+------------------
 10935 | {}
 10981 | {10935}
(2 rows)
```
Теперь следующая транзакция пытается обновить ту же строку:
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;
```
Что увидим в pg\_locks на этот раз?
```sql
=> SELECT pid, locktype, page, tuple, transactionid AS xid,
    mode, granted
FROM pg_locks WHERE locktype IN ('transactionid','tuple')
ORDER BY pid, granted DESC, locktype;
```

## Page 25: Page 25 {#source-page-025}

```text
  pid  |   locktype    | page | tuple | xid |     mode      | granted
-------+---------------+------+-------+-----+---------------+---------
 10935 | transactionid |      |       | 829 | ExclusiveLock | t
 10981 | transactionid |      |       | 830 | ExclusiveLock | t
 10981 | tuple         |    0 |     1 |     | ExclusiveLock | t
 10981 | transactionid |      |       | 829 | ShareLock     | f
 11421 | transactionid |      |       | 831 | ExclusiveLock | t
 11421 | tuple         |    0 |     1 |     | ExclusiveLock | f
(6 rows)
```
Транзакция встала в очередь за блокировкой версии строки, очередь выросла:
```sql
=> SELECT pid, pg_blocking_pids(pid) FROM pg_stat_activity
WHERE pid IN (10935,10981,11421) ORDER BY pid;
```
```text
  pid  | pg_blocking_pids
-------+------------------
 10935 | {}
 10981 | {10935}
 11421 | {10981}
(3 rows)
```
Если теперь первая транзакция успешно завершается...
```text
=> COMMIT;
```
```text
COMMIT
```
```sql
UPDATE 1
```
...выполняется UPDATE и первая версия становится неактуальной.
```sql
=> SELECT pid, locktype, page, tuple, transactionid AS xid,
    mode, granted
FROM pg_locks WHERE locktype IN ('transactionid','tuple')
ORDER BY pid, granted DESC, locktype;
```
```text
  pid  |   locktype    | page | tuple | xid |     mode      | granted
-------+---------------+------+-------+-----+---------------+---------
 10981 | transactionid |      |       | 830 | ExclusiveLock | t
 11421 | transactionid |      |       | 831 | ExclusiveLock | t
 11421 | transactionid |      |       | 830 | ShareLock     | f
(3 rows)
```
Все транзакции, которые стояли в очереди, будут теперь ожидать завершения второй транзакции — и будут обрабатываться в произвольном порядке. Вот как выглядит очередь в нашем случае:
```sql
=> SELECT pid, pg_blocking_pids(pid) FROM pg_stat_activity
WHERE pid IN (10981,11421) ORDER BY pid;
```
```text
  pid  | pg_blocking_pids
-------+------------------
 10981 | {}
 11421 | {10981}
(2 rows)
```
```text
=> COMMIT;
```
```text
COMMIT
```
```sql
UPDATE 1
```
```text
=> COMMIT;
```
```text
COMMIT
```

## Page 26: Итоги {#source-page-026}

**Блокировки отношений и других объектов БД используются для организации конкурентного доступа к общим ресурсам**
  - хранятся в разделяемой памяти сервера
  - имеется механизм очередей
- Блокировки строк реализованы иначе
  - хранятся в страницах данных из-за потенциально большого количества
  - используют блокировки уровня объектов для организации «очереди»

## Page 27: Практика {#source-page-027}

1. Какие блокировки на уровне изоляции Read Committed удерживает транзакция, прочитавшая одну строку таблицы по первичному ключу? Проверьте на практике. 2. Посмотрите, как в представлении pg\_locks отображаются рекомендательные блокировки. 3. Убедитесь на практике, что проверка внешнего ключа и обновление строки могут выполняться одновременно. Изучите возникающие при этом блокировки уровня строки. 4. Воспроизведите ситуацию взаимоблокировки двух транзакций и проверьте, как она обрабатывается сервером.

### Комментарии преподавателя

2. Рекомендательные блокировки упоминались в теме «Обработка ошибок» курса DEV1.

<https://postgrespro.ru/docs/postgresql/16/functions-admin#FUNCTIONS-AD> VISORY-LOCKS

Посмотрите все столбцы представления pg\_locks, чтобы определить, в каком из них отображается идентификатор ресурса.

3. Для этого потребуется создать две таблицы, связанные ограничением внешнего ключа.

Для анализа блокировок используйте расширение pgrowlocks.

4. Взаимоблокировка двух транзакций возникает, когда

● первая транзакция удерживает блокировку объектов, необходимых второй транзакции для продолжения работы,

● вторая транзакция удерживает блокировку объектов, необходимых первой транзакции для продолжения работы.

В общем случае может произойти взаимоблокировка более чем двух транзакций.

## Page 28: 1. Блокировки при чтении строки по первичному ключу {#source-page-028}

```sql
=> CREATE DATABASE arch_locks;
```
```sql
CREATE DATABASE
```
```sql
=> \c arch_locks
```
```text
You are now connected to database "arch_locks" as user "student".
```
```sql
=> SELECT pg_backend_pid();
```
```text
 pg_backend_pid
----------------
          56563
(1 row)
```
Создадим таблицу как в демонстрации:
```sql
=> CREATE TABLE accounts(acc_no integer PRIMARY KEY, amount numeric);
```
```sql
CREATE TABLE
```
```sql
=> INSERT INTO accounts VALUES (1,100.00), (2,200.00), (3,300.00);
```
```sql
INSERT 0 3
```
Прочитаем строку, начав транзакцию:
```sql
=> \c arch_locks
```
```text
You are now connected to database "arch_locks" as user "student".
```
```sql
=> SELECT pg_backend_pid();
```
```text
 pg_backend_pid
----------------
          56707
(1 row)
```
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> SELECT * FROM accounts WHERE acc_no = 1;
```
```text
 acc_no | amount
--------+--------
      1 | 100.00
(1 row)
```
Блокировки включают блокировку индекса, поддерживающего ограничение первичного ключа, в режиме Access Share:
```sql
=> SELECT locktype, relation::REGCLASS, virtualxid AS virtxid,
    transactionid AS xid, mode, granted
FROM pg_locks
WHERE pid = 56707;
```
```text
  locktype  |   relation    | virtxid | xid |      mode       | granted
------------+---------------+---------+-----+-----------------+---------
 relation   | accounts_pkey |         |     | AccessShareLock | t
 relation   | accounts      |         |     | AccessShareLock | t
 virtualxid |               | 3/15    |     | ExclusiveLock   | t
(3 rows)
```
```text
=> COMMIT;
```
```text
COMMIT
```
### 2. Рекомендательные блокировки
Захватим рекомендательную блокировку уровня сеанса:
```text
=> BEGIN;
```
```text
BEGIN
```

## Page 29: Page 29 {#source-page-029}

```sql
=> SELECT pg_advisory_lock(42);
```
```text
 pg_advisory_lock
------------------
(1 row)
```
Блокировка:
```sql
=> SELECT locktype, virtualxid AS virtxid, objid, mode, granted
FROM pg_locks
WHERE pid = 56707;
```
```text
  locktype  | virtxid | objid |     mode      | granted
------------+---------+-------+---------------+---------
 virtualxid | 3/16    |       | ExclusiveLock | t
 advisory   |         |    42 | ExclusiveLock | t
(2 rows)
```
Идентификатор ресурса для блокировки типа advisory отображается в столбце objid.
```text
=> COMMIT;
```
```text
COMMIT
```
### 3. Проверка внешнего ключа
Нам понадобится расширение для анализа блокировок на уровне строк:
```sql
=> CREATE EXTENSION pgrowlocks;
```
```sql
CREATE EXTENSION
```
Создадим таблицу клиентов:
```sql
=> CREATE TABLE clients(
    client_id integer PRIMARY KEY,
    name text
);
```
```sql
CREATE TABLE
```
```sql
=> INSERT INTO clients VALUES (10,'alice'), (20,'bob');
```
```sql
INSERT 0 2
```
В таблицу счетов добавим столбец для идентификатора клиента и внешний ключ:
```sql
=> ALTER TABLE accounts
    ADD client_id integer REFERENCES clients(client_id);
```
```sql
ALTER TABLE
```
Внутри транзакции выполним какое-нибудь действие с таблицей счетов, вызывающее проверку внешнего ключа. Например, вставим строку:
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> INSERT INTO accounts(acc_no, amount, client_id)
    VALUES (4,400.00,20);
```
```sql
INSERT 0 1
```
Проверка внешнего ключа приводит к появлению блокировки строки в таблице клиентов в режиме KeyShare:
```sql
=> SELECT * FROM pgrowlocks('clients') \gx
```
```text
-[ RECORD 1 ]-----------------
locked_row | (0,2)
locker     | 822
multi      | f
xids       | {822}
modes      | {"For Key Share"}
pids       | {56707}
```
Это не мешает изменять неключевые столбцы этой строки:
```sql
=> UPDATE clients SET name = 'brian' WHERE client_id = 20;
```
```sql
UPDATE 1
```

## Page 30: 4. Взаимоблокировка двух транзакций {#source-page-030}

```text
=> COMMIT;
```
```text
COMMIT
```
Обычная причина возникновения взаимоблокировок — разный порядок блокирования строк таблиц в приложении.
Первая транзакция намерена перенести 100 рублей с первого счета на второй. Для этого она сначала уменьшает первый счет:
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> UPDATE accounts SET amount = amount - 100.00 WHERE acc_no = 1;
```
```sql
UPDATE 1
```
В это же время вторая транзакция намерена перенести 10 рублей со второго счета на первый. Она начинает с того, что уменьшает второй счет:
```text
=> BEGIN;
```
```text
BEGIN
```
```sql
=> UPDATE accounts SET amount = amount - 10.00 WHERE acc_no = 2;
```
```sql
UPDATE 1
```
Теперь первая транзакция пытается увеличить второй счет...
```sql
=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 2;
```
...но обнаруживает, что строка заблокирована.
Затем вторая транзакция пытается увеличить первый счет...
```sql
=> UPDATE accounts SET amount = amount + 10.00 WHERE acc_no = 1;
```
...но тоже блокируется.
Возникает циклическое ожидание, которое никогда не завершится само по себе. Поэтому если какая-либо блокировка не получена за время, указанное в параметре deadlock\_timeout (по умолчанию — 1 секунда), сервер проверяет наличие циклов ожидания. Обнаружив такой цикл, он прерывает одну из транзакций, чтобы остальные могли продолжить работу.
```text
ERROR:  deadlock detected
DETAIL:  Process 56707 waits for ShareLock on transaction 824; blocked by process 56563.
Process 56563 waits for ShareLock on transaction 825; blocked by process 56707.
HINT:  See server log for query details.
CONTEXT:  while updating tuple (0,1) in relation "accounts"
```
```sql
UPDATE 1
```
```text
=> COMMIT;
```
```text
COMMIT
```
```text
=> COMMIT;
```
```text
ROLLBACK
```
Взаимоблокировки обычно означают, что приложение спроектировано неправильно. Правильный способ выполнения таких операций — блокирование ресурсов в одном и том же порядке. Например, в данном случае можно блокировать счета в порядке возрастания их номеров.
Тем не менее взаимоблокировки могут возникать и при нормальной работе (например, могут взаимозаблокироваться две команды UPDATE одной и той же таблицы). Но это очень редкие ситуации.
