# Блокировки отношений

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

---

<span id="internal-ru-chapter-12" class="pdf-anchor"></span><span id="internal-ru-page-256" class="pdf-anchor"></span>

<span id="internal-ru-dummy-93" class="pdf-anchor"></span>

## 12.1. Общие сведения о блокировках {#internal-ru-section-12-1}

*Блокировки* (locks), называемые также *замка́ми*, упорядочивают конкурентный доступ к разделяемым ресурсам.

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

Перед тем как обратиться к ресурсу, процесс обязан *захватить* (acquire) блокировку, ассоциированную с этим ресурсом, а по окончании работы — *освободить* (release) ее, чтобы ресурсом могли воспользоваться другие. Когда блокировками управляет СУБД, установленный порядок поддерживается автоматически; если блокировки устанавливает приложение, то обязанность соблюдать правила ложится на него.

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

> [!NOTE]
> Можно заметить, что такой участок разделяемой памяти сам по себе является ресурсом. Конкурентный доступ к нему упорядочивается с помощью специальных примитивов синхронизации (таких как семафоры или мьютексы), предоставляемых операционной системой. Они обеспечивают строго последовательное выполнение кода, обращающегося к разделяемому ресурсу. На самом низком уровне эти примитивы

<span id="internal-ru-page-257" class="pdf-anchor"></span>

> [!NOTE]
> реализуются на основе атомарных инструкций процессора (таких как test-and-set или compare-and-swap).

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

Например, ресурсом может быть объект, с которым работает СУБД, такой как таблица (идентифицируется с помощью `oid` в системном каталоге), страница данных (идентифицируется именем файла и позицией внутри файла), версия строки (идентифицируется страницей и смещением внутри страницы). Ресурсом может быть структура в памяти, такая как хеш-таблица или буфер (идентифицируется заранее присвоенным номером). Иногда даже бывает удобно использовать абстрактные ресурсы, не имеющие никакого физического смысла.

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

Выделю два фактора, сильно влияющих на эффективность блокировок.

**Гранулярность** — степень детализации блокировки. Гранулярность важна, если ресурсы образуют иерархию.

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

Блокировка отдельных строк (мелкая гранулярность) лишена этого недостатка, но количество блокировок сильно увеличивается. Чтобы информация о них не заняла слишком много памяти, могут применяться разные методы, например *повышение уровня* (эскалация): когда количество низкоуровневых, мелкогранулярных блокировок превышает определенный предел, они заменяются на одну блокировку более высокого уровня.

<span id="internal-ru-page-258" class="pdf-anchor"></span>

**Набор режимов,** в которых могут захватываться блокировки.

Часто применяют всего два режима. *Исключительный* (exclusive), или *монопольный*, режим несовместим ни с каким другим режимом, в том числе с самим собой. В *разделяемом* (shared) режиме блокировка может захватываться несколькими процессами одновременно. Разделяемый режим может использоваться для чтения ресурса, а исключительный — для изменения.

В общем случае режимов может быть больше. Имена режимов не имеют значения, важна лишь матрица их совместимости друг с другом.

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

Блокировки можно классифицировать по времени использования.

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

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

**Короткие** блокировки захватываются на доли секунды (а зачастую — на время выполнения нескольких инструкций процессора) и обычно относятся к структурам данных в общей памяти. Такими блокировками СУБД управляют полностью автоматически.

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

<span id="internal-ru-page-259" class="pdf-anchor"></span>

В PostgreSQL используются самые разные виды блокировок[^internal-ru-258-1]. К длительным относятся *«тяжелые» блокировки* (на уровне отношений и других объектов); отдельно выделяются *блокировки на уровне строк*. К коротким блокировкам относятся различные *блокировки в оперативной памяти*. Особняком стоят *предикатные блокировки*, которые, несмотря на название, блокировками не являются. (*с.* [268](/ru/13-row-level-locks/#internal-ru-page-269)) (*с.* [305](/ru/15-locks-on-memory-structures/#internal-ru-page-306)) (*с.* [298](/ru/14-miscellaneous-locks/#internal-ru-page-299))

## 12.2. Тяжелые блокировки {#internal-ru-section-12-2}

*Тяжелые* (heavyweight) блокировки относятся к длительным. Они устанавливаются на уровне *объектов*: в первую очередь отношений, но также и некоторых других. Эти блокировки обычно защищают объекты от одновременного изменения или от использования во время реорганизации, но могут применяться и для других нужд. Такая нечеткая формулировка связана с тем, что блокировки из этой группы используются для самых разных целей, а объединяет их лишь то, как они устроены.

Обычно под словом «блокировка», если нет явных уточнений, понимаются именно такие, тяжелые блокировки.

Тяжелые блокировки располагаются в общей памяти сервера [^internal-ru-259-1] и доступны для изучения в представлении `pg_locks`. Их количество ограничено произведением значений двух параметров сервера: *max\_locks\_per\_transaction* и *max\_connections*. (*по умолчанию:* `64`) (*по умолчанию:* `100`)

Пул блокировок — общий для всех транзакций, то есть одна транзакция может захватить больше блокировок, чем *max\_locks\_per\_transaction*. Важно лишь, чтобы общее число блокировок в системе не превысило установленный предел. Пул создается при запуске, так что изменение любого из двух указанных параметров требует перезагрузки сервера.

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

<span id="internal-ru-page-260" class="pdf-anchor"></span>

Возможна ситуация *взаимоблокировки* (deadlock), или *тупика* двух транзакций, при которой первой из них для продолжения работы требуется ресурс, занятый второй транзакцией, а второй в это время необходим ресурс, занятый первой. Это простой случай; попасть в тупиковую ситуацию могут и более двух транзакций. При взаимоблокировке ожидание длилось бы бесконечно, поэтому PostgreSQL автоматически определяет тупиковые ситуации и аварийно прерывает одну из транзакций, чтобы остальные могли продолжить работу. (*с.* [286](/ru/13-row-level-locks/#internal-ru-page-287))

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

В приведенном списке названия соответствуют значениям столбца `locktype` представления `pg_locks`:

**transactionid** и **virtualxid** — блокировка номера транзакции; (*с.* [259](/ru/12-relation-level-locks/#internal-ru-page-260))

**relation** — блокировка отношения; (*с.* [261](/ru/12-relation-level-locks/#internal-ru-page-262))

**tuple** — блокировка версии строки; (*с.* [275](/ru/13-row-level-locks/#internal-ru-page-276))

**object** — блокировка объекта, который не является отношением; (*с.* [293](/ru/14-miscellaneous-locks/#internal-ru-page-294))

**extend** — блокировка файлов отношений при добавлении новых страниц; (*с.* [295](/ru/14-miscellaneous-locks/#internal-ru-page-296))

**page** — блокировка страницы (используется некоторыми типами индексов); (*с.* [296](/ru/14-miscellaneous-locks/#internal-ru-page-297))

**advisory** — рекомендательная блокировка. (*с.* [296](/ru/14-miscellaneous-locks/#internal-ru-page-297))

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

## 12.3. Блокировки номеров транзакций {#internal-ru-section-12-3}

<span id="internal-ru-dummy-94" class="pdf-anchor"></span>

Каждая транзакция всегда удерживает исключительную блокировку своего собственного номера (и виртуального, и — если есть — реального). (*с.* [91](/ru/03-pages-and-tuples/#internal-ru-page-92))

<span id="internal-ru-page-261" class="pdf-anchor"></span>

Используются два режима блокировки, исключительный и разделяемый. Матрица конфликтов очень проста: разделяемый режим совместим сам с собой; исключительный режим несовместим ни с каким режимом.

| Режим | Shared | Exclusive |
| --- | --- | --- |
| Shared |  | × |
| Exclusive | × | × |
{#internal-ru-table-12-1 num="12.1" caption="Матрица конфликтов: Shared и Exclusive" class="wide"}

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

Начнем транзакцию в отдельном сеансе и получим номер обслуживающего процесса:

```text {label="psql"}
=> BEGIN;
=> SELECT pg_backend_pid();
 pg_backend_pid
----------------
          64412
(1 row)
```

Только что начатая транзакция удерживает исключительную блокировку собственного виртуального номера:

```text {label="psql"}
=> SELECT locktype, virtualxid, mode, granted
FROM pg_locks WHERE pid = 64412;
  locktype  | virtualxid |     mode      | granted
------------+------------+---------------+---------
 virtualxid | 4/2        | ExclusiveLock | t
(1 row)
```

Здесь `locktype` — это тип блокировки, `virtualxid` — виртуальный номер транзакции (идентифицирующий ресурс), `mode` — режим (в данном случае исключительный). Флаг `granted` показывает, удалось ли получить запрашиваемую блокировку.

<span id="internal-ru-page-262" class="pdf-anchor"></span>

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

```text {label="psql"}
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
             191442
(1 row)
```

```text {label="psql"}
=> SELECT locktype, virtualxid, transactionid AS xid, mode, granted
FROM pg_locks WHERE pid = 64412;
   locktype    | virtualxid |  xid   |     mode      | granted
---------------+------------+--------+---------------+---------
 virtualxid    | 4/2        |        | ExclusiveLock | t
 transactionid |            | 191442 | ExclusiveLock | t
(2 rows)
```

Теперь транзакция удерживает исключительную блокировку обоих своих номеров.

## 12.4. Блокировки отношений {#internal-ru-section-12-4}

<span id="internal-ru-dummy-95" class="pdf-anchor"></span>

Для блокировки отношений определено целых восемь различных режимов[^internal-ru-262-1]. Такое разнообразие необходимо для того, чтобы как можно большее количество команд, относящихся к одному отношению (таблице, индексу или другому объекту), могло выполняться одновременно.

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

| Режим | AS | RS | RE | SUE | S | SRE | E | AE | Команды |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Access Share |  |  |  |  |  |  |  | × | SELECT |
| Row Share |  |  |  |  |  |  | × | × | SELECT FOR UPDATE/SHARE |
| Row Exclusive |  |  |  |  | × | × | × | × | INSERT, UPDATE, DELETE |
| Share Update Exclusive |  |  |  | × | × | × | × | × | VACUUM, CREATE INDEX CONCURRENTLY |
| Share |  |  | × | × |  | × | × | × | CREATE INDEX |
| Share Row Exclusive |  |  | × | × | × | × | × | × | CREATE TRIGGER |
| Exclusive |  | × | × | × | × | × | × | × | REFRESH MAT. VIEW CONCURRENTLY |
| Access Exclusive | × | × | × | × | × | × | × | × | DROP, TRUNCATE, VACUUM FULL, LOCK TABLE, REFRESH MAT. VIEW |
{#internal-ru-table-12-2 num="12.2" caption="Матрица конфликтов блокировок отношений" class="wide"}

<span id="internal-ru-page-263" class="pdf-anchor"></span>

Первый режим, Access Share, самый слабый. Он совместим с любым другим, кроме последнего, Access Exclusive. Поэтому команда `SELECT` не мешает выполнению почти никаких команд, но не даст, например, удалить таблицы, к которым обращается запрос. Режим Access Exclusive, наоборот, самый строгий: он несовместим ни с одним режимом, то есть является исключительным.

Первые четыре режима допускают одновременное изменение данных в таблице, а следующие четыре — нет. Например, команда `CREATE INDEX` использует режим Share. Он совместим сам с собой (то есть можно одновременно создавать несколько индексов для одной таблицы) и с режимами читающих команд. Таким образом, команды `SELECT` будут выполняться, а вот команды `INSERT`, `UPDATE` и `DELETE` будут заблокированы.

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

Команда `ALTER TABLE` имеет много вариантов, которые требуют разных режимов блокировки (Share Update Exclusive, Share Row Exclusive, Access Exclusive). Все режимы, разумеется, описаны в документации[^internal-ru-263-1].

<span id="internal-ru-page-264" class="pdf-anchor"></span>

Примеры в этой части книги будут использовать уже знакомую нам таблицу счетов:

```text {label="psql"}
=> TRUNCATE accounts;
=> INSERT INTO accounts(id, client, amount)
VALUES (1, 'alice',   100.00),
       (2, 'bob',     200.00),
       (3, 'charlie', 300.00);
```

Запрос к таблице блокировок понадобится еще не раз, поэтому создадим для него представление, в котором для краткости вывода свернем все идентификаторы в один общий столбец:

```text {label="psql"}
=> CREATE VIEW locks AS
SELECT pid,
  locktype,
  CASE locktype
    WHEN 'relation' THEN relation::regclass::text
    WHEN 'transactionid' THEN transactionid::text
    WHEN 'virtualxid' THEN virtualxid
  END AS lockid,
  mode,
  granted
FROM pg_locks
ORDER BY 1, 2, 3;
```

Транзакция в первом сеансе (она не завершалась) обновляет строку. Вместе с таблицей блокируются и все индексы этой таблицы, поэтому появляются две новые блокировки с типом `relation` и режимом Row Exclusive:

```text {label="psql"}
=> UPDATE accounts SET amount = amount + 100.00 WHERE id = 1;
```

```text {label="psql"}
=> SELECT locktype, lockid, mode, granted
FROM locks WHERE pid = 64412;
   locktype    |    lockid     |       mode       | granted
---------------+---------------+------------------+---------
 relation      | accounts      | RowExclusiveLock | t
 relation      | accounts_pkey | RowExclusiveLock | t
 transactionid | 191442        | ExclusiveLock    | t
 virtualxid    | 4/2           | ExclusiveLock    | t
(4 rows)
```

<span id="internal-ru-page-265" class="pdf-anchor"></span>

## 12.5. Очередь ожидания {#internal-ru-section-12-5}

Тяжелые блокировки предоставляют «честную» очередь ожидания [^internal-ru-265-1]. Процесс встает в очередь, если пытается захватить блокировку, несовместимую с режимом уже захваченной блокировки или с режимом любого из процессов, уже ожидающих в очереди.

Пока в первом сеансе транзакция выполняет обновление, попробуем в другом сеансе создать индекс по таблице:

```text {label="psql"}
=> SELECT pg_backend_pid();
 pg_backend_pid
----------------
          64897
(1 row)
=> CREATE INDEX ON accounts(client);
```

Команда «подвисает» в ожидании освобождения ресурса. Транзакция пытается получить блокировку таблицы в режиме Share, но не может:

```text {label="psql" hl_lines="5"}
=> SELECT locktype, lockid, mode, granted
FROM locks WHERE pid = 64897;
  locktype  |  lockid  |     mode      | granted
------------+----------+---------------+---------
 relation   | accounts | ShareLock     | f
 virtualxid | 6/3      | ExclusiveLock | t
(2 rows)
```

Пусть теперь в третьем сеансе будет запущена команда `VACUUM FULL`. Она тоже встанет в очередь, поскольку необходимый ей режим Access Exclusive несовместим ни с одним режимом:

<span id="internal-ru-page-266" class="pdf-anchor"></span>

```text {label="psql" hl_lines="11"}
      => SELECT pg_backend_pid();
       pg_backend_pid
      ----------------
                65106
      (1 row)
      => VACUUM FULL accounts;
=> SELECT locktype, lockid, mode, granted
FROM locks WHERE pid = 65106;
   locktype    |  lockid  |        mode         | granted
---------------+----------+---------------------+---------
 relation      | accounts | AccessExclusiveLock | f
 transactionid | 191446   | ExclusiveLock       | t
 virtualxid    | 8/4      | ExclusiveLock       | t
(3 rows)
```

Теперь и все последующие претенденты будут попадать в очередь, уже независимо от режима блокировки. Даже обычные запросы `SELECT` будут честно вставать за `VACUUM FULL`, хоть и совместимы с блокировкой Row Exclusive, которую удерживает первый сеанс, выполняющий команду `UPDATE`.

```text {label="psql"}
=> SELECT pg_backend_pid();
 pg_backend_pid
----------------
          65322
(1 row)
=> SELECT * FROM accounts;
```

```text {label="psql" hl_lines="5"}
=> SELECT locktype, lockid, mode, granted
FROM locks WHERE pid = 65322;
  locktype  |  lockid  |      mode       | granted
------------+----------+-----------------+---------
 relation   | accounts | AccessShareLock | f
 virtualxid | 10/3     | ExclusiveLock   | t
(2 rows)
```

![Очередь ожидания: T1; UPDATE; relation; T2; CREATE INDEX; T3; VACUUM FULL; T4; SELECT](/images/internal/ru/p266-01.svg)
{#internal-ru-fig-12-1 num="12.1" caption="Очередь ожидания" .book-original-figure width=552 height=290}

<span id="internal-ru-page-267" class="pdf-anchor"></span>

Общую картину ожиданий дает функция `pg_blocking_pids`. Она показывает номера процессов, которые стоят в очереди перед указанным и либо удерживают, либо запрашивают несовместимую блокировку:

```text {label="psql"}
=> SELECT pid,
  pg_blocking_pids(pid),
  wait_event_type,
  state,
  left(query,50) AS query
FROM pg_stat_activity
WHERE pid IN (64412,64897,65106,65322) \gx
-[ RECORD 1 ]----+---------------------------------------------------
pid              | 64412
pg_blocking_pids | {}
wait_event_type  | Client
state            | idle in transaction
query            | UPDATE accounts SET amount = amount + 100.00 WHERE
-[ RECORD 2 ]----+---------------------------------------------------
pid              | 64897
pg_blocking_pids | {64412}
wait_event_type  | Lock
state            | active
query            | CREATE INDEX ON accounts(client);
-[ RECORD 3 ]----+---------------------------------------------------
pid              | 65106
pg_blocking_pids | {64412,64897}
wait_event_type  | Lock
state            | active
query            | VACUUM FULL accounts;
-[ RECORD 4 ]----+---------------------------------------------------
pid              | 65322
pg_blocking_pids | {65106}
wait_event_type  | Lock
state            | active
query            | SELECT * FROM accounts;
```

Более детальное представление можно получить, аккуратно анализируя содержимое таблицы `pg_locks`[^internal-ru-267-1].

Когда транзакция завершается (неважно, фиксацией или обрывом), все ее блокировки снимаются[^internal-ru-267-2]. Процесс, стоящий первым в очереди, получает запрашиваемую блокировку и пробуждается.

<span id="internal-ru-page-268" class="pdf-anchor"></span>

> [!NOTE]
> Неочевидный момент состоит в том, что при откате к точке сохранения (командой `ROLLBACK TO SAVEPOINT` или при попадании в секцию `EXCEPTION` PL/pgSQL-блока) снимаются все блокировки, захваченные транзакцией с момента установки этой точки. Это связано с откатом вложенной транзакции и освобождением ее блокировок.

В нашем примере завершение транзакции в первом сеансе приведет к последовательному выполнению всех запросов, стоящих в очереди: (*с.* [92](/ru/03-pages-and-tuples/#internal-ru-page-93))

```text {label="psql"}
=> ROLLBACK;
ROLLBACK
```

```sql
CREATE INDEX
```

```sql
VACUUM
```

```text
 id | client  | amount
----+---------+--------
  1 | alice   | 100.00
  2 | bob     | 200.00
  3 | charlie | 300.00
(3 rows)
```

[^internal-ru-258-1]: [backend/storage/lmgr/README](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/README;hb=REL_18_STABLE>).
[^internal-ru-259-1]: [backend/storage/lmgr/lock.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/lock.c;hb=REL_18_STABLE>).
[^internal-ru-262-1]: [postgrespro.ru/docs/postgresql/18/explicit-locking#LOCKING-TABLES](<https://postgrespro.ru/docs/postgresql/18/explicit-locking#LOCKING-TABLES>).
[^internal-ru-263-1]: [postgrespro.ru/docs/postgresql/18/sql-altertable](<https://postgrespro.ru/docs/postgresql/18/sql-altertable>).
[^internal-ru-265-1]: [backend/storage/lmgr/lock.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/lock.c;hb=REL_18_STABLE>), функция LockAcquire.
[^internal-ru-267-1]: [wiki.postgresql.org/wiki/Lock\_dependency\_information](<https://wiki.postgresql.org/wiki/Lock_dependency_information>).
[^internal-ru-267-2]: [backend/storage/lmgr/lock.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/storage/lmgr/lock.c;hb=REL_18_STABLE>), функции LockReleaseAll и LockRelease.
