Привет, Хабр!

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) примитивы синхронизации». Записаться