Привет, Хабр!
PostgreSQL умеет блокировать строки (SELECT ... FOR UPDATE) и таблицы (LOCK TABLE). Об этом знают все. Но есть третий тип блокировок, который решает задачи, с которыми row-level и table-level locks не справляются: advisory locks. Консультативные блокировки — механизм, где PostgreSQL предоставляет инфраструктуру (атомарные блокировки с очередями ожидания), а семантику определяет приложение.
Это значит: вы берёте блокировку по произвольному числовому ключу, и PostgreSQL гарантирует, что никто другой не возьмёт блокировку с тем же ключом одновременно. Никаких таблиц, строк или ресурсов БД не блокируется — это чисто логическая блокировка, видимая только тем, кто её проверяет.
Звучит как-то абстрактно. Посмотрим на конкретные задачи, где advisory locks незаменимы.
Задача: дедупликация обработки событий
Ситуация: очередь задач в таблице PostgreSQL. Несколько воркеров берут задачи и обрабатывают. Как гарантировать, что два воркера не возьмут одну задачу?
Стандартный подход — SELECT ... FOR UPDATE SKIP LOCKED:
BEGIN; SELECT id, payload FROM tasks WHERE status = 'pending' ORDER BY created_at LIMIT 1 FOR UPDATE SKIP LOCKED; -- обработка... UPDATE tasks SET status = 'done' WHERE id = 42; COMMIT;
Работает, но есть ограничение: блокировка живёт до конца транзакции. Если обработка занимает 30 секунд, транзакция открыта 30 секунд. Длинные транзакции мешают автовакууму, раздувают таблицу и увеличивают вероятность дедлоков.
Advisory lock решает иначе:
-- Берём advisory lock по ID задачи (неблокирующая попытка) SELECT pg_try_advisory_lock(42); -- Вернёт true, если блокировка получена, false — если кто-то уже держит -- Если получили — обрабатываем (в отдельной короткой транзакции) UPDATE tasks SET status = 'processing' WHERE id = 42; COMMIT; -- ... долгая обработка вне транзакции ... -- Финализируем BEGIN; UPDATE tasks SET status = 'done' WHERE id = 42; COMMIT; -- Отпускаем advisory lock SELECT pg_advisory_unlock(42);
Транзакции короткие — только UPDATE. Блокировка живёт на уровне сессии, независимо от транзакций. Два воркера не возьмут задачу 42 одновременно, потому что pg_try_advisory_lock(42) вернёт false второму.
Два режима: session-level и transaction-level
Session-level (по умолчанию): блокировка живёт до явного pg_advisory_unlock() или до закрытия сессии.
SELECT pg_advisory_lock(123); -- Блокирующий вызов (ждёт, если занято) SELECT pg_try_advisory_lock(123); -- Неблокирующий (true/false) SELECT pg_advisory_unlock(123); -- Явное освобождение
Transaction-level: блокировка автоматически снимается при COMMIT/ROLLBACK.
SELECT pg_advisory_xact_lock(123); -- Блокирующий, до конца транзакции SELECT pg_try_advisory_xact_lock(123); -- Неблокирующий, до конца транзакции
Transaction-level удобнее, когда блокировка нужна ровно на время транзакции — не нужно помнить про unlock. Session-level нужен, когда блокировка переживает несколько транзакций, как в примере с очередью выше.
Ключи: одно число или два
Advisory lock принимает один bigint или два int:
-- Один ключ (bigint) SELECT pg_advisory_lock(42); -- Два ключа (int, int) — удобно для «тип ресурса + ID» SELECT pg_advisory_lock(1, 42); -- тип=1 (заказ), ID=42 SELECT pg_advisory_lock(2, 42); -- тип=2 (клиент), ID=42 -- Это РАЗНЫЕ блокировки, несмотря на одинаковый второй аргумент
Два ключа удобны для разделения пространства блокировок. Первый — тип сущности (заказ, клиент, отчёт), второй — ID. Это исключает любые коллизии: блокировка заказа 42 и клиента 42 — абсолютно разные блокировки.
Если ID — UUID или строка, хешируйте:
-- Строковый ключ через hashtext SELECT pg_advisory_lock(hashtext('order:abc-123-def'));
hashtext возвращает int, что достаточно для advisory lock. Коллизии теоретически возможны (birthday paradox), но для базовых масштабов практически невероятны.
Задача: singleton-процесс (только один экземпляр)
Крон-задача, которая должна запускаться строго в одном экземпляре — даже если сервер приложений масштабирован на 10 инстансов:
import psycopg2 conn = psycopg2.connect(dsn) cur = conn.cursor() # Пытаемся взять блокировку (неблокирующая) cur.execute("SELECT pg_try_advisory_lock(999999)") acquired = cur.fetchone()[0] if not acquired: print("Другой экземпляр уже работает, выходим") exit(0) try: run_import_job() # Долгая работа finally: cur.execute("SELECT pg_advisory_unlock(999999)") conn.close()
Десять инстансов стартуют одновременно. Только один получит блокировку 999999. Остальные девять увидят false и завершатся. Никаких файловых локов, Redis, ZooKeeper — всё в PostgreSQL, которая уже есть в стеке.
Задача: предотвращение двойной оплаты (idempotency)
Пользователь нажимает «Оплатить» дважды. Два запроса приходят параллельно. Оба проверяют «заказ не оплачен?», оба видят «нет», оба выполняют оплату. Деньги списаны дважды.
-- В начале обработки платежа SELECT pg_advisory_xact_lock(hashtext('payment:order:' || order_id::text)); -- Теперь проверяем статус и обрабатываем -- Второй параллельный запрос будет ждать на pg_advisory_xact_lock -- К моменту, когда он получит блокировку, первый уже обновит статус SELECT status FROM orders WHERE id = $1; -- Если уже 'paid' — выходим -- Если 'pending' — обрабатываем UPDATE orders SET status = 'paid' WHERE id = $1; COMMIT; -- Блокировка снимется автоматически
Это проще и надёжнее, чем SELECT ... FOR UPDATE (который блокирует строку, а не логическую операцию), и проще, чем unique constraint на таблице платежей (который требует обработки конфликта).
Задача: rate limiting на уровне БД
Нужно ограничить параллельное выполнение тяжёлых отчётов до 3 одновременных:
-- Пытаемся занять один из трёх «слотов» SELECT pg_try_advisory_lock(hashtext('heavy_report'), slot) FROM generate_series(1, 3) AS slot LIMIT 1; -- Если получили хотя бы один слот — выполняем -- Если все три заняты — отказ
Три «слота» — три advisory lock с разными вторыми ключами. Пока все три заняты, четвёртый запрос получит false и может вернуть пользователю «система загружена, попробуйте позже».
Мониторинг
Advisory locks видны в pg_locks:
SELECT pid, locktype, classid, -- первый ключ (для двухключевых) objid, -- второй ключ (или единственный для bigint) granted, mode FROM pg_locks WHERE locktype = 'advisory';
Если advisory locks зависают (сессия взяла и не отпустила), они будут видны здесь. Session-level блокировки — опасны: если приложение упало без pg_advisory_unlock, блокировка живёт до закрытия сессии. Если используется пул соединений (PgBouncer) — сессия может жить вечно, и блокировка вместе с ней.
Используйте pg_advisory_xact_lock (transaction-level) там, где возможно. Для session-level — всегда unlock в finally-блоке.
Проблемы
Пулы соединений. PgBouncer в режиме transaction переиспользует соединения между транзакциями. Session-level advisory lock, взятый в одной транзакции, останется на соединении, когда его получит другой клиент. Решение: transaction-level locks в transaction-режиме PgBouncer, или session-mode PgBouncer для воркеров, которые держат session-level блокировки.
Рекурсивность. Advisory locks в PostgreSQL рекурсивны: если сессия вызовет pg_advisory_lock(42) дважды, для разблокировки нужно вызвать pg_advisory_unlock(42) тоже дважды.
Коллизии ключей. Пространство ключей — bigint (или пара int). Если две разные сущности случайно получат одинаковый ключ — они заблокируют друг друга. Используйте разные пространства ключей (двухключевую форму с типом ресурса как первым ключом).
Не блокируют данные. Advisory lock не мешает другим транзакциям читать или писать в таблицы. Он блокирует только тех, кто проверяет тот же advisory lock. Если одна часть кода использует advisory lock, а другая — нет, блокировка бесполезна. Вся логика, работающая с ресурсом, должна проходить через один и тот же advisory lock.

Практические курсы по аналитике от экспертов — в каталоге. Подберите маршрут обучения под свою роль: от базовой подготовки до задач в рабочей среде.
А чтобы узнать больше о формате обучения и задать вопросы экспертам, приходите на бесплатные уроки:
9 апреля, 20:00. «PostgreSQL как векторная база данных: ИИ-поиск без лишних сервисов». Записаться
15 апреля, 20:00. «Основы проектирования бизнес-логики в микросервисной архитектуре». Записаться
16 апреля, 20:00. «Облегчённые (Slim) примитивы синхронизации». Записаться
