Блокировки и взаимоблокировки

Рейтинг: 78.5% · 14 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Блокировки и взаимоблокировки

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки (вы здесь)
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 36. Блокировки и взаимоблокировки

Когда десятки сессий правят одни и те же данные, postgresql должен решить, кто ждёт, а кто работает прямо сейчас. За это отвечают блокировки. Этот урок про то, как читать и понимать механику: чем блокировка строки отличается от блокировки таблицы, почему две честные транзакции вдруг убивают друг друга через deadlock, что такое advisory locks и как находить узкие места через системное представление pg_locks. Цель - научиться писать конкурентный код без неприятных сюрпризов под нагрузкой.

Изображение

Как это работает

Блокировка - это пометка о намерении, которую сессия ставит на объект, чтобы другие сессии знали, можно ли им трогать тот же объект сейчас. PostgreSQL держит таблицу блокировок в общей памяти и при каждой операции проверяет совместимость: два чтения уживаются, а запись и запись по одной строке - уже конфликт. Важная мысль: обычные SELECT в PostgreSQL почти никогда никого не ждут, потому что многоверсионность (MVCC) даёт читателю старую видимую версию строки, а не заставляет ждать писателя.

Блокировки строк нужны, когда вы хотите застолбить конкретную запись до конца своей транзакции. Команда SELECT ... FOR UPDATE говорит: эту строку я собираюсь менять, другие пусть подождут. Более мягкий вариант FOR SHARE разрешает другим тоже читать с FOR SHARE, но не даёт никому её обновить или удалить - типичный приём, чтобы родительская строка не исчезла, пока вы вставляете дочернюю. Есть ещё FOR NO KEY UPDATE и FOR KEY SHARE - это более слабые формы, которые PostgreSQL использует внутри для внешних ключей, чтобы не блокировать лишнего.

Блокировки таблиц - это уровень выше. Их PostgreSQL берёт автоматически: SELECT берёт самый слабый режим ACCESS SHARE, UPDATE/DELETE/INSERT берут ROW EXCLUSIVE, а команды вроде ALTER TABLE или TRUNCATE берут самый сильный ACCESS EXCLUSIVE, несовместимый вообще ни с чем, включая чтение. Команда LOCK TABLE позволяет взять табличный режим вручную, но это нужно редко и опасно - легко превратить параллельную систему в очередь из одного человека.

Взаимоблокировка (deadlock) возникает, когда транзакция А держит ресурс 1 и ждёт ресурс 2, а транзакция Б держит ресурс 2 и ждёт ресурс 1. Никто не уступит сам. PostgreSQL не предотвращает такие ситуации заранее, он их обнаруживает: специальный механизм периодически (по умолчанию через deadlock_timeout, обычно 1 секунда) строит граф ожиданий и ищет цикл. Найдя, он выбирает жертву, откатывает её транзакцию с ошибкой и тем разрывает кольцо. Ваша задача - поймать эту ошибку и повторить транзакцию.

Advisory locks (рекомендательные блокировки) - это блокировки, которые не привязаны ни к какой строке или таблице. Это просто числовой ключ, который вы блокируете сами по своей логике: например, чтобы только один воркер обрабатывал очередь с заданным id. PostgreSQL их никак не интерпретирует, он лишь гарантирует, что один и тот же ключ одновременно держит только одна сессия.

SQL и примеры

Возьмём демобазу Авиаперевозки (схема bookings). Захватим конкретное бронирование, чтобы безопасно его пересчитать. Другая сессия с тем же FOR UPDATE будет ждать нашего COMMIT:

Код: Выделить всё

BEGIN;
SELECT book_ref, total_amount
FROM bookings.bookings
WHERE book_ref = '0824C5'
FOR UPDATE;
-- строка наша до конца транзакции
UPDATE bookings.bookings
SET total_amount = total_amount + 100.00
WHERE book_ref = '0824C5';
COMMIT;
Чтобы не выстраивать очередь, а быстро пропустить занятые строки, удобен SKIP LOCKED - классический паттерн обработки очереди задач несколькими воркерами:

Код: Выделить всё

SELECT ticket_no
FROM bookings.tickets
WHERE book_ref = '0824C5'
FOR UPDATE SKIP LOCKED;
Если ждать совсем нельзя, используйте NOWAIT - он сразу вернёт ошибку вместо ожидания:

Код: Выделить всё

SELECT * FROM bookings.flights
WHERE flight_id = 12345
FOR UPDATE NOWAIT;
Посмотреть, кто кого ждёт прямо сейчас, можно через pg_locks в связке с pg_stat_activity. Этот запрос показывает заблокированные сессии и тех, кто их держит:

Код: Выделить всё

SELECT blocked.pid          AS blocked_pid,
       blocked.query        AS blocked_query,
       blocking.pid         AS blocking_pid,
       blocking.query       AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
  ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type = 'Lock';
Функция pg_blocking_pids появилась давно и куда удобнее ручного разбора pg_locks. Advisory lock берётся так - один воркер захватит ключ, остальные с pg_try_advisory_lock получат false и займутся другим:

Код: Выделить всё

SELECT pg_try_advisory_lock(42);   -- true у первого, false у остальных
-- ... работа ...
SELECT pg_advisory_unlock(42);
Частые грабли
  • Блокировки строки живут до конца транзакции, а не до конца запроса. Долгая открытая транзакция (забыли COMMIT) держит строки и плодит ожидания у всех соседей.
  • Deadlock - это не баг, а нормальная ситуация под нагрузкой. Код обязан ловить ошибку с кодом 40P01 и повторять транзакцию, иначе пользователь увидит сбой.
  • Главное лекарство от deadlock - всегда блокировать строки в одном и том же порядке (например, по возрастанию id). Разный порядок в разных частях кода - прямой путь к кольцу.
  • LOCK TABLE ... ACCESS EXCLUSIVE останавливает даже читателей. Случайно взятый такой режим (часто через неаккуратный ALTER TABLE) кладёт под нагрузкой весь сервис.
  • FOR UPDATE внутри запроса с JOIN по умолчанию блокирует строки всех таблиц. Если нужна только одна, пишите FOR UPDATE OF имя_таблицы.
  • Долгий ALTER TABLE встаёт в очередь за ACCESS EXCLUSIVE и блокирует все запросы, которые пришли ПОСЛЕ него, даже если сам ещё ждёт. Для миграций ставьте lock_timeout.
  • Advisory locks уровня сессии не снимаются при откате транзакции - их надо отпускать руками или брать xact-версии (pg_advisory_xact_lock), которые уходят с концом транзакции.
Мини-лаба
  • Откройте два терминала psql к своей базе (для 1С-инсталляций тот же приём работает поверх рабочей БД, только осторожно на проде).
  • В первой сессии: BEGIN; затем SELECT ... FOR UPDATE по одной строке демобазы. Не коммитьте.
  • Во второй сессии повторите тот же SELECT ... FOR UPDATE - он повиснет в ожидании.
  • В третьей сессии (или новом окне) выполните запрос из примера с pg_blocking_pids и найдите пару blocked/blocking.
  • Сделайте COMMIT в первой сессии и убедитесь, что вторая разблокировалась мгновенно.
  • Воспроизведите deadlock: две сессии блокируют две разные строки, затем каждая пытается взять строку другой. Поймайте сообщение об ошибке deadlock detected и посмотрите детали в логе сервера.
  • Поэкспериментируйте с FOR UPDATE NOWAIT и FOR UPDATE SKIP LOCKED, сравните поведение.
Контрольные вопросы
  • Почему обычный SELECT в PostgreSQL обычно не ждёт писателя, и при чём тут MVCC?
  • Чем отличаются FOR UPDATE, FOR SHARE и FOR NO KEY UPDATE по силе блокировки?
  • Как PostgreSQL обнаруживает deadlock и по какому таймауту он начинает проверку?
  • В чём разница между SKIP LOCKED и NOWAIT, и для каких задач каждый из них?
  • Почему ACCESS EXCLUSIVE опаснее ROW EXCLUSIVE и какие команды его берут?
  • Чем advisory lock уровня сессии отличается от xact-версии и где это критично?
👍5 ❤️3 🔥2 😄 🤔1
Аватара пользователя
grpcmaker
Сообщения: 1
Зарегистрирован: 27 май 2026, 08:57

Re: Блокировки и взаимоблокировки

Сообщение grpcmaker »

А если ловить 40P01 и ретраить, не получится ли бесконечный цикл, когда оба воркера упрямо лезут в том же порядке?
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
bash_kun
Сообщения: 1
Зарегистрирован: 27 май 2026, 23:12

Re: Блокировки и взаимоблокировки

Сообщение bash_kun »

Проверил SKIP LOCKED на очереди заданий - три воркера разобрали строки без единого ожидания, красота. Жаль раньше не знал.
👍 ❤️1 🔥 😄 🤔1
Ответить
← Предыдущая глава
WAL, контрольные точки и долговечность
Следующая глава →
Планировщик запросов

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: уровни изоляции транзакций и блокировки в postgresql

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость