lesson ── postgres-labs ── ~22 мин ── 5 шагов
Два «перевода денег» в обратном порядке - и получаем цикл, который не
разрешится сам. PostgreSQL подождёт deadlock_timeout (1 секунда),
обнаружит цикл и откатит одну из транзакций. Ты соберёшь это руками,
поймаешь ошибку 40P01, прочитаешь описание цикла в логе сервера и
убедишься по счётчику pg_stat_database.deadlocks, что тупик случился.
Вкладки: psql-a и psql-b - две конкурирующие транзакции, psql-ops
psql.интерактивный sandbox
Поднимется контейнер postgreslab/postgres-base с PostgreSQL 17 и psql. В браузере откроется терминал, база lab уже настроена. Каждый шаг проверяется автоматически. Сеть air-gapped, наружу контейнер не ходит.
stack ── PostgreSQL 17 · psql · 1 GB RAM · air-gapped · самоуничтожается через 45 мин простоя
Вкладка psql-a, запусти psql:
BEGIN;
UPDATE bookings SET total = total - 10 WHERE book_ref = '000001';
Строка 000001 теперь заблокирована транзакцией A (её номер записан
в xmax строки). Транзакция открыта - не коммить.
UPDATE блокирует затронутую строку до конца транзакции.
✓ A держит строку 000001.
Вкладка psql-b, запусти psql:
BEGIN;
UPDATE bookings SET total = total - 10 WHERE book_ref = '000002';
Теперь каждая транзакция держит «свою» строку. Цикла пока нет - обе ещё могут завершиться.
Две открытые транзакции, каждая со своей заблокированной строкой.
✓ B держит строку 000002. Сцена готова.
Вернись в psql-a:
UPDATE bookings SET total = total + 10 WHERE book_ref = '000002';
Команда повисла: строку 000002 держит B. A ждёт завершения
транзакции B. Предскажи: сколько сессий сейчас в ожидании
wait_event_type = 'Lock'?
A ждёт транзакцию B - это ожидание тяжёлой блокировки, тип Lock.
✓ A стоит в ожидании. Осталось замкнуть цикл.
Вернись в psql-b и потяни за строку, которую держит A:
UPDATE bookings SET total = total + 10 WHERE book_ref = '000001';
Теперь A ждёт B, а B ждёт A - цикл. Примерно через секунду
(deadlock_timeout) одна из транзакций получит
ERROR: deadlock detected. Проверим счётчик базы.
Детектор запускается не сразу, а спустя deadlock_timeout (1 с).
✓ deadlocks > 0 - сервер обнаружил и разорвал цикл.
Открой лог сервера (вкладка pg в окне сессии или docker logs
ноды pg) и найди блок:
ERROR: deadlock detected
DETAIL: Process ... waits for ... blocked by process ...
CONTEXT: while updating tuple (...) in relation "bookings"
DETAIL описывает цикл по PID, CONTEXT называет конкретный
кортеж. Выжившая транзакция получила свою строку - закоммить её
(COMMIT в той вкладке, где не было ошибки). Жертва откатилась
целиком. Проверим, что в очереди никто не висит.
После разрыва цикла ни одна сессия не должна ждать Lock.
✓ Очередь чиста. Жертва откатилась, выживший закоммитился.
Взаимоблокировка - цикл в графе ожидания, который сервер находит через deadlock_timeout и разрывает, откатывая одну транзакцию с ошибкой 40P01. Лог в DETAIL описывает цикл по PID, в CONTEXT - кортеж. Счётчик pg_stat_database.deadlocks подтверждает событие. Лекарство - единый порядок блокировки строк и ретрай на 40P01.
команды
SELECT deadlocks FROM pg_stat_database WHERE datname=current_database();счётчик взаимоблокировокSHOW deadlock_timeout;задержка перед запуском детектораSELECT ... ORDER BY id FOR UPDATE;блокировать в едином порядкеSELECT count(*) FROM pg_stat_activity WHERE wait_event_type='Lock';сколько сессий ждут блокировкуконцепции