open atlas
↑ К треку
SQL и PostgreSQL вглубь SQL · 07 · 04

Advisory-блокировки

Advisory-блокировки — мьютексы уровня приложения по целочисленному ключу, не привязанные к строке. Идеальны для leader election и дедупа cron — а сессионные тихо текут через transaction-pooler.

SQL Senior ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

Ночной биллинг-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-режимом пулера она может зависнуть на том, что позже переиспользует другой запрос — поэтому транзакционный вариант безопаснее по умолчанию.

Вспомните перед уходом
  1. 01
    Чем advisory-блокировка отличается от блокировки строки вроде SELECT FOR UPDATE?
  2. 02
    В чём разница между pg_advisory_lock и pg_advisory_xact_lock и какая безопасна с пулером?
  3. 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки
открывает

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.