Advisory-блокировки
Advisory-блокировки — мьютексы уровня приложения по целочисленному ключу, не привязанные к строке. Идеальны для leader election и дедупа cron — а сессионные тихо текут через transaction-pooler.
Ночной биллинг-cron работал на трёх app-серверах для надёжности. Однажды ночью все три сработали в одну минуту, и каждого клиента списали трижды. Первый фикс команды — SELECT FOR UPDATE на «строке-блокировке» — работал, пока они не перешли на PgBouncer в transaction-режиме: тогда блокировка тихо перестала держаться между запросами задачи. Настоящий инструмент они никогда не использовали — advisory-блокировка Postgres, мьютекс уровня приложения, вообще не привязанный к таблице. Но у advisory-блокировок есть своя ловушка, которая укусила их следующей.
Мьютекс, который не строка
Что блокировать, когда то, что ты координируешь, — не одна строка, а целая задача, пересборка индекса тенанта или право быть единственным сервером, делающим что-то прямо сейчас? Вот для чего нужны advisory-блокировки.
Каждая блокировка до сих пор защищала строку. Advisory-блокировка не защищает ничего конкретного — это именованная защёлка, о которой договаривается приложение, по 64-битному целому (или двум 32-битным), смысл которым задаёшь ты. Postgres лишь гарантирует не больше одного держателя данного ключа за раз, по всему кластеру. Это распределённый мьютекс, который ты получаешь бесплатно вместе с уже имеющейся базой.
-- Попытаться стать единственным воркером для "nightly-billing" (ключ 42).
SELECT pg_try_advisory_lock(42);
-- вернёт true -> ты держишь, делай работу
-- вернёт false -> держит кто-то другой, тихо пропусти
SELECT pg_advisory_unlock(42); -- освободить по завершенииpg_advisory_lock(key) блокируется до захвата; pg_try_advisory_lock(key) возвращается сразу с true/false. Неблокирующая форма try_ — то, что нужно для «это должен сделать только один из нас — остальные просто пропускают».
Сессионная против транзакционной
Именно это различие решает, работает ли твоя блокировка в продакшене.
- Сессионная (
pg_advisory_lock,pg_try_advisory_lock) — держится, пока ты явно не вызовешьpg_advisory_unlockили пока сессия (соединение с БД) не закончится. Она не освобождается наCOMMIT. - Транзакционная (
pg_advisory_xact_lock,pg_try_advisory_xact_lock) — автоматически освобождается в конце транзакции (COMMITилиROLLBACK). Нет функции unlock и нет способа её утечь.
-- Транзакционная: освобождается автоматически на COMMIT/ROLLBACK. Безопасна с пулером.
BEGIN;
SELECT pg_try_advisory_xact_lock(42);
-- ... делай защищённую работу ...
COMMIT; -- блокировка гарантированно ушла, даже если код забыл unlockГде advisory-блокировки сияют
- Leader election / дедуп cron — N реплик каждая делает
pg_try_advisory_lock(key); та, что получитtrue, выполняет singleton-задачу, остальные пропускают. Без ZooKeeper, без Redis. - Мьютекс уровня приложения над логическим ресурсом — сериализовать путь кода по чему-то, что не одна строка: «перестроить поисковый индекс тенанта 99» →
pg_advisory_xact_lock(99). - Сериализация горячего пути без блокировки реальных строк данных, избегая bloat и FK-конкуренции.
Против блокировок строк: блокировка строки требует существования строки и защищает именно тот кортеж; advisory-блокировка защищает абстрактный ключ, который может не соответствовать никакой строке, будущей строке или целой логической операции. Используй блокировки строк для данных, которые редактируешь; advisory-блокировки для координации.
▸Частая ошибка
Ловушка пулера целиком. Сессионные advisory-блокировки привязаны к соединению с БД, а не к логическому запросу приложения. Под transaction-режимом пулера (PgBouncer pool_mode = transaction, RDS Proxy, Supabase pooler) приложение одалживает backend-соединение на транзакцию и возвращает его в пул после — так что сессионная блокировка, взятая в одной транзакции, может держаться на соединении, которое теперь переиспользует другой запрос, или никогда не освободиться, потому что «твою» сессию передали дальше. Получаешь фантомную конкуренцию и блокировки, переживающие своего владельца. Два правила: (1) предпочитай pg_advisory_xact_lock, чтобы блокировка умирала с транзакцией и не зависала на пулинговом соединении; (2) если приходится использовать сессионную блокировку, не запускай этот путь кода за transaction-режимом пулера. Также берегись коллизий пространства ключей: две несвязанные фичи, обе хэширующие имя в один 64-битный ключ, будут блокировать друг друга без видимой причины — давай ключам неймспейс (например, форму из двух int с classid на фичу).
Твой сервис работает за PgBouncer в transaction-режиме. Какой вызов advisory-блокировки безопасен и почему?
Пять реплик приложения каждая запускают одну и ту же ежечасную задачу очистки. Нужно, чтобы ровно одна выполнила её в час, остальные тихо пропустили. Лучший примитив?
Заполни пропуск: сессионная advisory-блокировка привязана к _______ с БД, так что под transaction-режимом пулера она может зависнуть на том, что позже переиспользует другой запрос — поэтому транзакционный вариант безопаснее по умолчанию.
- 01Чем advisory-блокировка отличается от блокировки строки вроде SELECT FOR UPDATE?
- 02В чём разница между pg_advisory_lock и pg_advisory_xact_lock и какая безопасна с пулером?
- 03Назови два специфичных для advisory-блокировок режима отказа и как их избежать.
Advisory-блокировка — мьютекс, который Postgres держит над определённым тобой целочисленным ключом, отвязанный от любой строки — база становится распределённым менеджером блокировок, которым ты уже управляешь. Блокирующий pg_advisory_lock и неблокирующий pg_try_advisory_lock покрывают «ждать её» против «пропустить, если занята»; для singleton’ов на весь флот (leader election, дедуп cron) форма try_ даёт ровно одной реплике выиграть, а остальным уйти. Выбор, решающий продакшн-надёжность, — область: сессионные блокировки переживают COMMIT и привязаны к физическому соединению, так что transaction-режим пулера может их зависить или утечь; транзакционная pg_advisory_xact_lock освобождается в конце транзакции и безопасна с пулером, что делает её верным умолчанием. Помимо ловушки пулера, давай ключам неймспейс, чтобы две фичи не столкнулись на одном целом, и тянись к advisory-блокировкам только для координации — блокировки строк остаются инструментом для охраны данных, которые ты реально редактируешь. Дальше — отказ, возникающий, когда блокировки любого вида берутся в конфликтующем порядке: deadlock’и. Теперь, когда увидишь cron, отработавший трижды, или сессионную блокировку, непонятно удержавшуюся дольше своей транзакции, — проверь, стоит ли код за transaction-режимом пулера, и переключись на pg_advisory_xact_lock.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.