Блокировок строк недостаточно, когда нужно синхронизировать не конкретную запись, а логическую операцию. Например, два воркера одновременно начинают генерацию одного и того же отчёта для пользователя. Даже если результат в итоге сохраняется с
UNIQUE(user_id), это не предотвращает двойное выполнение дорогостоящей работы.Для таких случаев PostgreSQL предоставляет advisory locks — блокировки, ключ и семантику которых определяет приложение:
SELECT pg_advisory_lock(1001);
Если другая сессия запросит lock с тем же ключом, она будет ждать его освобождения.
pg_advisory_lock() работает на уровне сессии: блокировка сохраняется до явного pg_advisory_unlock() или завершения сессии:SELECT pg_advisory_unlock(1001);
Если критическая секция укладывается в транзакцию, обычно удобнее
pg_advisory_xact_lock(): такая блокировка автоматически освобождается при COMMIT или ROLLBACK:BEGIN;
SELECT pg_advisory_xact_lock(1, 1001);
-- Здесь выполняется операция, которую нужно сериализовать.
INSERT INTO reports(user_id, created_at)
SELECT 1001, now()
WHERE NOT EXISTS (
SELECT 1
FROM reports
WHERE user_id = 1001
);
COMMIT;
Важно то, что операция, которую нужно защитить от конкурентного выполнения, должна происходить после получения lock и до его освобождения.
Если дорогостоящая работа выполняется приложением вне транзакции и может занимать значительное время, держать ради неё долгую транзакцию обычно нежелательно. В таком случае можно использовать session-level advisory lock и гарантированно освобождать его после завершения критической секции.
Здесь
1 можно использовать как namespace операции, а 1001 — как идентификатор ресурса:(1, 1001) — generate_report / user 1001
(2, 1001) — recalculate_stats / user 1001
Так независимые операции над одним ресурсом не будут случайно блокировать друг друга.
Если воркеру не нужно ждать освобождения lock, есть неблокирующий вариант:
SELECT pg_try_advisory_xact_lock(1, 1001);
Он сразу вернёт:
true — lock получен, выполняем работу
false — lock уже удерживается, работу можно пропустить
Это удобно для cron-задач и фоновых воркеров, где второй экземпляр работы не должен ждать завершения первого.
advisory lock не гарантирует уникальность данных. Он координирует только процессы, которые используют одинаковый протокол блокировок. Инварианты данных по-прежнему должны обеспечиваться самой БД:
ALTER TABLE reports
ADD CONSTRAINT reports_user_id_key UNIQUE (user_id);
PostgreSQL не знает, что означает
(1, 1001). Для него это просто ключ блокировки. Поэтому все конкурирующие процессы должны одинаково формировать ключи и захватывать соответствующие locks.🔥 Advisory locks полезны для генерации артефактов, фоновых задач, пересчётов, cron jobs и других операций, где критическая секция существует на уровне бизнес-логики, а не отдельной строки таблицы.
➡️ SQL Ready | #практика