Когда десятки сессий правят одни и те же данные, postgresql должен решить, кто ждёт, а кто работает прямо сейчас. За это отвечают блокировки. Этот урок про то, как читать и понимать механику: чем блокировка строки отличается от блокировки таблицы, почему две честные транзакции вдруг убивают друг друга через deadlock, что такое advisory locks и как находить узкие места через системное представление pg_locks. Цель - научиться писать конкурентный код без неприятных сюрпризов под нагрузкой.

Как это работает
Блокировка - это пометка о намерении, которую сессия ставит на объект, чтобы другие сессии знали, можно ли им трогать тот же объект сейчас. PostgreSQL держит таблицу блокировок в общей памяти и при каждой операции проверяет совместимость: два чтения уживаются, а запись и запись по одной строке - уже конфликт. Важная мысль: обычные SELECT в PostgreSQL почти никогда никого не ждут, потому что многоверсионность (MVCC) даёт читателю старую видимую версию строки, а не заставляет ждать писателя.
Блокировки строк нужны, когда вы хотите застолбить конкретную запись до конца своей транзакции. Команда SELECT ... FOR UPDATE говорит: эту строку я собираюсь менять, другие пусть подождут. Более мягкий вариант FOR SHARE разрешает другим тоже читать с FOR SHARE, но не даёт никому её обновить или удалить - типичный приём, чтобы родительская строка не исчезла, пока вы вставляете дочернюю. Есть ещё FOR NO KEY UPDATE и FOR KEY SHARE - это более слабые формы, которые PostgreSQL использует внутри для внешних ключей, чтобы не блокировать лишнего.
Блокировки таблиц - это уровень выше. Их PostgreSQL берёт автоматически: SELECT берёт самый слабый режим ACCESS SHARE, UPDATE/DELETE/INSERT берут ROW EXCLUSIVE, а команды вроде ALTER TABLE или TRUNCATE берут самый сильный ACCESS EXCLUSIVE, несовместимый вообще ни с чем, включая чтение. Команда LOCK TABLE позволяет взять табличный режим вручную, но это нужно редко и опасно - легко превратить параллельную систему в очередь из одного человека.
Взаимоблокировка (deadlock) возникает, когда транзакция А держит ресурс 1 и ждёт ресурс 2, а транзакция Б держит ресурс 2 и ждёт ресурс 1. Никто не уступит сам. PostgreSQL не предотвращает такие ситуации заранее, он их обнаруживает: специальный механизм периодически (по умолчанию через deadlock_timeout, обычно 1 секунда) строит граф ожиданий и ищет цикл. Найдя, он выбирает жертву, откатывает её транзакцию с ошибкой и тем разрывает кольцо. Ваша задача - поймать эту ошибку и повторить транзакцию.
Advisory locks (рекомендательные блокировки) - это блокировки, которые не привязаны ни к какой строке или таблице. Это просто числовой ключ, который вы блокируете сами по своей логике: например, чтобы только один воркер обрабатывал очередь с заданным id. PostgreSQL их никак не интерпретирует, он лишь гарантирует, что один и тот же ключ одновременно держит только одна сессия.
SQL и примеры
Возьмём демобазу Авиаперевозки (схема bookings). Захватим конкретное бронирование, чтобы безопасно его пересчитать. Другая сессия с тем же FOR UPDATE будет ждать нашего COMMIT:
Код: Выделить всё
BEGIN;
SELECT book_ref, total_amount
FROM bookings.bookings
WHERE book_ref = '0824C5'
FOR UPDATE;
-- строка наша до конца транзакции
UPDATE bookings.bookings
SET total_amount = total_amount + 100.00
WHERE book_ref = '0824C5';
COMMIT;
Код: Выделить всё
SELECT ticket_no
FROM bookings.tickets
WHERE book_ref = '0824C5'
FOR UPDATE SKIP LOCKED;
Код: Выделить всё
SELECT * FROM bookings.flights
WHERE flight_id = 12345
FOR UPDATE NOWAIT;
Код: Выделить всё
SELECT blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type = 'Lock';
Код: Выделить всё
SELECT pg_try_advisory_lock(42); -- true у первого, false у остальных
-- ... работа ...
SELECT pg_advisory_unlock(42);
- Блокировки строки живут до конца транзакции, а не до конца запроса. Долгая открытая транзакция (забыли COMMIT) держит строки и плодит ожидания у всех соседей.
- Deadlock - это не баг, а нормальная ситуация под нагрузкой. Код обязан ловить ошибку с кодом 40P01 и повторять транзакцию, иначе пользователь увидит сбой.
- Главное лекарство от deadlock - всегда блокировать строки в одном и том же порядке (например, по возрастанию id). Разный порядок в разных частях кода - прямой путь к кольцу.
- LOCK TABLE ... ACCESS EXCLUSIVE останавливает даже читателей. Случайно взятый такой режим (часто через неаккуратный ALTER TABLE) кладёт под нагрузкой весь сервис.
- FOR UPDATE внутри запроса с JOIN по умолчанию блокирует строки всех таблиц. Если нужна только одна, пишите FOR UPDATE OF имя_таблицы.
- Долгий ALTER TABLE встаёт в очередь за ACCESS EXCLUSIVE и блокирует все запросы, которые пришли ПОСЛЕ него, даже если сам ещё ждёт. Для миграций ставьте lock_timeout.
- Advisory locks уровня сессии не снимаются при откате транзакции - их надо отпускать руками или брать xact-версии (pg_advisory_xact_lock), которые уходят с концом транзакции.
- Откройте два терминала psql к своей базе (для 1С-инсталляций тот же приём работает поверх рабочей БД, только осторожно на проде).
- В первой сессии: BEGIN; затем SELECT ... FOR UPDATE по одной строке демобазы. Не коммитьте.
- Во второй сессии повторите тот же SELECT ... FOR UPDATE - он повиснет в ожидании.
- В третьей сессии (или новом окне) выполните запрос из примера с pg_blocking_pids и найдите пару blocked/blocking.
- Сделайте COMMIT в первой сессии и убедитесь, что вторая разблокировалась мгновенно.
- Воспроизведите deadlock: две сессии блокируют две разные строки, затем каждая пытается взять строку другой. Поймайте сообщение об ошибке deadlock detected и посмотрите детали в логе сервера.
- Поэкспериментируйте с FOR UPDATE NOWAIT и FOR UPDATE SKIP LOCKED, сравните поведение.
- Почему обычный SELECT в PostgreSQL обычно не ждёт писателя, и при чём тут MVCC?
- Чем отличаются FOR UPDATE, FOR SHARE и FOR NO KEY UPDATE по силе блокировки?
- Как PostgreSQL обнаруживает deadlock и по какому таймауту он начинает проверку?
- В чём разница между SKIP LOCKED и NOWAIT, и для каких задач каждый из них?
- Почему ACCESS EXCLUSIVE опаснее ROW EXCLUSIVE и какие команды его берут?
- Чем advisory lock уровня сессии отличается от xact-версии и где это критично?