PostgreSQL CookbookТранзакцииДедлоки и advisory-локи
0 / 63 (0%)

Дедлоки и advisory-локи

Два процесса Brew обрабатывают возвраты. Первый блокирует строку заказа, потом тянется к строке товара. Второй — наоборот: сначала товар, потом заказ. По отдельности код безупречен. Но если они стартуют одновременно, случается худшее: первый держит заказ и ждёт товар, второй держит товар и ждёт заказ. Каждый ждёт того, что держит другой. Никто не отпустит первым — они застряли навечно. Это дедлок (deadlock), взаимная блокировка.

Хорошая новость: Postgres не зависнет. Он обнаруживает цикл блокировок сам (по таймеру deadlock_timeout, по умолчанию 1 секунда) и разрывает его — выбирает «жертву» и завершает её транзакцию ошибкой 40P01 (deadlock_detected), вторая проходит. Этот юнит — про две вещи: про 40P01 (что это и откуда) и про pg_advisory_lock — прикладную блокировку, которой дедлоки чаще всего предотвращают.

Это escape-hatch-юнит (как 05-02): дедлок конкурентен по природе, ведём урок psql-скриптами.

Дедлок и его профилактика

Дедлоку нужен «крест»: две транзакции захватывают одни и те же ресурсы в противоположном порядке. Отсюда и главное лекарство — единый порядок блокировок. Если оба процесса возвратов всегда берут сначала заказ, потом товар (например, по возрастанию id), цикл не образуется: тот, кто первым взял заказ, спокойно возьмёт и товар, второй подождёт его и пойдёт следом. Дедлоки в приложениях на 90% лечатся дисциплиной «блокируй ресурсы в одном и том же порядке».

plaintext
   Транзакция A ──держит──▶ строка #1
       │                        ▲
     хочет                    хочет
       ▼                        │
   строка #2 ◀──держит── Транзакция B

A держит #1 и хочет #2; B держит #2 и хочет #1 — стрелки «хочет» идут навстречу, это и есть дедлок-«крест». Каждый ждёт того, что держит другой, цикл замкнут → 40P01. Возьми обе транзакции ресурсы в одном порядке (сначала #1, потом #2) — и стрелки «хочет» больше не пересекаются: цикла нет.

40P01 — транзиентная ошибка, как и 40001 из 05-04/05-05: жертве достаточно повторить транзакцию (та же ретрай-петля). Но в отличие от serialization failure, дедлок — это почти всегда сигнал кривого порядка блокировок, а не неизбежность; повтор спасает здесь и сейчас, но чинить надо порядок.

Advisory-лок: блокировка не для данных, а для логики

Иногда нужно сериализовать не строку, а операцию: «пусть пересчёт остатков кофейни #7 выполняет только один воркер за раз», даже если он трогает десятки строк. Городить для этого блокировку какой-то одной «сторожевой» строки — хрупко. Postgres даёт честный инструмент — advisory-лок: именованную защёлку по произвольному 64-битному ключу, смысл которого знает только приложение.

sql
SELECT pg_try_advisory_lock(42);  -- t — взяли, f — занято (без ожидания)
-- ... критическая секция ...
SELECT pg_advisory_unlock(42);    -- отпустили

Advisory-локи бывают двух видов по времени жизни: session-level (pg_advisory_lock/pg_try_advisory_lock) живёт до явного unlock или конца коннекта, и транзакционный (pg_advisory_xact_lock) — освобождается сам на COMMIT/ROLLBACK. Второй безопаснее: забыть отпустить нельзя. Именно одним advisory-локом как «воротами» можно и предотвратить наш дедлок: если обе транзакции сперва берут общий pg_advisory_xact_lock(returns_key), они выстраиваются в очередь и в принципе не схлёстываются на строках.

Три функции advisory-API — по ожиданию и времени жизни:

ФункцияЖдёт, если занято?Время жизниСнимается
pg_advisory_lock(key)да, блокируетсяsession-levelявным pg_advisory_unlock или концом коннекта
pg_try_advisory_lock(key)нет — сразу t/fsession-levelто же
pg_advisory_xact_lock(key)да, блокируетсятранзакциясам на COMMIT/ROLLBACK

Что показывает наш код

demo.sql (цель run) — детерминированный тур по API advisory-локов на одной сессии: взять/повторно взять (реентрабельность), отпустить нужное число раз (лишний unlock → f + WARNING), и транзакционный лок, который снимается сам на COMMIT.

session-a.sql / session-b.sql — живой дедлок в двух терминалах: A берёт строку #1 потом тянется к #2, B берёт #2 потом тянется к #1. Цикл замыкается — и Postgres завершает одну из транзакций с 40P01.

Запуск

Подними песочницу (из корня репозитория) и накати схему Brew:

sh
docker compose up -d
make lecture L=05-transactions-and-mvcc/05-06-deadlocks-and-advisory-locks T=db-reset

Детерминированное демо API advisory-локов:

sh
make lecture L=05-transactions-and-mvcc/05-06-deadlocks-and-advisory-locks
plaintext
── Берём session-level advisory-лок по ключу 42 ──
 got_42 
--------
 t
(1 row)
 
 
── Та же сессия берёт ключ 42 повторно (реентрабельно) → счётчик = 2 ──
 got_42_again 
--------------
 t
(1 row)
 
 
── Отпускаем дважды (t, t); третий unlock → f (лок уже не наш; +WARNING в stderr) ──
 unlock_1 
----------
 t
(1 row)
 
 unlock_2 
----------
 t
(1 row)
 
 unlock_3 
----------
 f
(1 row)
 
 
── Транзакционный лок по ключу 7: живёт до COMMIT, освобождается сам ──
 pg_advisory_xact_lock 
-----------------------
 
(1 row)
 
 held_now 
----------
        1
(1 row)
 
 held_after_commit 
-------------------
                 0
(1 row)
 
→ held_now=1 (внутри tx), held_after_commit=0 (COMMIT снял лок автоматически).

Теперь живой дедлок. В первом терминале запусти session-a: A возьмёт строку #1 и остановится у подсказки. Во втором терминале прогони session-b до его подсказки — B возьмёт строку #2. Вернись в A, нажми Enter — A потянется за #2 и зависнет (её держит B). Вернись в B, нажми Enter — B потянется за #1, цикл замкнётся, и Postgres разорвёт дедлок:

plaintext
A2) Тянемся за строкой #2 (её держит B) → A ЗАВИСАЕТ в ожидании.
    Как только B потянется за строкой #1, цикл замкнётся, и Postgres разорвёт дедлок:
ERROR:  deadlock detected
DETAIL:  Process 1863 waits for ShareLock on transaction 832; blocked by process 1864.
Process 1864 waits for ShareLock on transaction 831; blocked by process 1863.
HINT:  See server log for query details.
CONTEXT:  while updating tuple (0,2) in relation "lock_lab"

Postgres сам выбрал жертву (здесь — A) и откатил её транзакцию; B при этом прошла и закоммитила. (Номера процессов/транзакций в DETAIL будут свои, и жертвой может оказаться B — выбор за планировщиком.) После демо накати схему Brew заново: make ... T=db-reset.

Заборчик

Порядок шагов в сессиях держат \prompt — в реальной гонке всё происходит за миллисекунды, и какую транзакцию назначить жертвой, решает Postgres (как правило — ту, что откатить «дешевле»). А вот что стоит помнить:

  • Дедлок возможен не только на строках. Внешние ключи без индекса, эскалация на уровне ALTER TABLE, те же advisory-локи, взятые крест-накрест, — везде, где есть блокировки.
  • Бороться лучше профилактикой, не ретраем. Единый порядок захвата, короткие транзакции, индексы под FK (см. 06). Ретрай на 40P01 — страховка, а не стратегия.
  • 40001 и 40P01 — разной природы. 40001 (05-04/05-05) — про логические конфликты сериализации, порядком блокировок его не предотвратить. 40P01 — про физический цикл ожидания, и вот он порядком как раз лечится.
  • Advisory-локи тоже не бесплатны. Session-level лок легко «утечёт» (забыли unlock, или коннект вернулся в пул с висящим локом) — поэтому в приложениях почти всегда берут pg_advisory_xact_lock, который снимается сам.

Что забрать с собой

  • Дедлок — две транзакции ждут ресурсы, захваченные друг другом во встречном порядке; никто не продолжит.
  • Postgres обнаруживает дедлок сам (по deadlock_timeout) и разрывает: жертве — 40P01 (deadlock_detected), вторая проходит.
  • Лекарство-профилактика — единый порядок блокировок (например, по возрастанию id); ретрай на 40P01 — страховка, а не замена дисциплине.
  • Advisory-лок (pg_advisory_lock / pg_advisory_xact_lock) — прикладная блокировка по числовому ключу, не привязанная к строкам: сериализует операцию. Транзакционный вариант освобождается сам на COMMIT — предпочтителен.
  • 40P01 (физический цикл ожидания) лечится порядком блокировок; 40001 (логический конфликт сериализации) — нет. Оба транзиентны и оба ретраятся.

Это последний юнит модуля 05. Дальше — модуль 06 «Индексы и EXPLAIN»: блокировки и снимки разобраны, пора понять, как Postgres находит строки, и научиться читать план запроса (EXPLAIN ANALYZE с буферами — в PG18 по умолчанию).

·Модуль 06

Этот урок ещё впереди

Курс изучается по порядку — чтобы открыть этот шаг, сначала завершите предыдущие. Так контекст накапливается без пропусков.

/ вы пытались открыть
Транзакции / Дедлоки и advisory-локи