Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Транзакции и блокировки
transactions_locks

Транзакции и блокировки

Снимки данных, уровни изоляции, блокирование, взаимоблокировки и повторы

Открыть лабораториюv1.1.0Запускается локально из публичного репозитория

Транзакции и блокировки: когда клиенты работают одновременно

Транзакция — группа команд, которая фиксируется целиком через COMMIT или отменяется целиком через ROLLBACK. Это свойство называют атомарностью: внешний наблюдатель не должен увидеть половину перевода денег.

BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT;

Но атомарности недостаточно. Два клиента могут одновременно прочитать и изменить одни данные. Уровень изоляции определяет, какие результаты чужих транзакций видит текущая транзакция.

#MVCC и снимок данных

При UPDATE PostgreSQL обычно создаёт новую версию строки. Снимок данных (snapshot) определяет, какие версии видимы конкретной команде или транзакции. Так чтение часто не блокирует изменение, а изменение — чтение.

Служебные сведения можно увидеть так:

SELECT pg_current_xact_id(), pg_current_snapshot();

У версий строк есть системные поля xmin, xmax и физический адрес ctid. Они полезны для исследования внутреннего устройства, но не являются прикладными идентификаторами: ctid меняется после обновления и переписывания.

#READ COMMITTED: новый снимок для каждой команды

READ COMMITTED — стандартный уровень PostgreSQL. Каждая SQL-команда получает снимок зафиксированных данных на момент своего начала.

BEGIN ISOLATION LEVEL READ COMMITTED; SELECT balance FROM accounts WHERE id = 1; -- Здесь другая транзакция может изменить и зафиксировать баланс. SELECT balance FROM accounts WHERE id = 1; COMMIT;

Два SELECT одной транзакции могут увидеть разные значения. Они также могут увидеть разный набор строк, подходящих под одинаковый WHERE.

Название READ UNCOMMITTED PostgreSQL принимает, но обрабатывает так же, как READ COMMITTED: незавершённые чужие изменения не становятся видимыми.

Если UPDATE встречает строку, которую меняет другая транзакция, он ждёт. После фиксации конкурента PostgreSQL заново проверяет условие для актуальной версии. Поэтому безопасное списание можно выразить одной командой:

UPDATE accounts SET balance = balance - 100 WHERE id = $1 AND balance >= 100 RETURNING balance;

Если денег уже не хватает, команда изменит ноль строк. Проверка и списание не разорваны промежутком, в который мог бы вмешаться другой клиент.

#REPEATABLE READ: один снимок на транзакцию

На уровне REPEATABLE READ снимок фиксируется первой обычной командой и используется до конца транзакции. Повторный запрос не увидит новые фиксации других транзакций.

PostgreSQL не допускает здесь грязного чтения, неповторяемого чтения и фантомов — новых строк, внезапно попавших под уже проверенное условие.

Но возможна аномалия разнесённой записи (write skew). Представим двух дежурных врачей. Каждая транзакция видит второго врача на дежурстве и снимает себя. Они меняют разные строки, поэтому прямого конфликта записи нет, но после обеих фиксаций не остаётся ни одного дежурного.

Значит, один устойчивый снимок не защищает автоматически правило между несколькими строками.

#SERIALIZABLE: результат как при последовательной работе

SERIALIZABLE — самый строгий уровень. PostgreSQL отслеживает зависимости чтения и записи. Если одновременный результат нельзя объяснить никаким последовательным порядком транзакций, одна из них получает ошибку SQLSTATE 40001.

Это ожидаемая часть работы уровня, а не поломка сервера. Приложение должно откатить и повторить всю транзакцию с новым снимком:

предел попыток → BEGIN → чтения и изменения → COMMIT ↘ 40001 → ROLLBACK → пауза → новый BEGIN

Повторять только последнюю команду внутри старой транзакции нельзя. Пауза обычно увеличивается с каждой попыткой и получает небольшую случайную добавку, чтобы конкуренты снова не столкнулись одновременно.

Сетевой вызов внутри повторяемой транзакции опасен: ROLLBACK базы не отменит уже отправленное письмо или платёж. Внешняя операция должна иметь ключ идемпотентности либо выполняться после записи в транзакционную исходящую очередь.

#Явная блокировка строки

Если решение зависит от прочитанного состояния и не помещается в один условный UPDATE, строку можно заблокировать:

BEGIN; SELECT available, reserved FROM inventory WHERE product_id = $1 FOR UPDATE; UPDATE inventory SET reserved = reserved + $2 WHERE product_id = $1; COMMIT;

FOR UPDATE не даёт другой транзакции одновременно изменить выбранную строку. Держите блокировку как можно меньше: не ждите внутри транзакции ответа сети или действия пользователя.

Если нужно заблокировать несколько строк, всегда берите их в одном порядке, например по возрастанию id. Более слабые режимы FOR NO KEY UPDATE, FOR SHARE и FOR KEY SHARE конфликтуют с меньшим числом действий; выбирайте минимальный режим, который защищает правило.

#Как найти блокирующий сеанс

Блокирование (blocking) означает, что один процесс ждёт ресурс, удерживаемый другим:

SELECT blocked.pid AS blocked_pid, blocked.wait_event_type, blocked.wait_event, pg_blocking_pids(blocked.pid) AS blockers, blocked.query FROM pg_stat_activity AS blocked WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;

Проверьте возраст транзакции, состояние idle in transaction, команду и владельца сеанса. Не завершайте процесс автоматически только из-за большого времени: откат огромной транзакции тоже может быть дорогим.

#Взаимоблокировка

Взаимоблокировка (deadlock) — цикл ожиданий:

T1 держит счёт 1 и ждёт счёт 2 T2 держит счёт 2 и ждёт счёт 1

Обе транзакции не могут продолжить. PostgreSQL обнаруживает цикл и отменяет одну с кодом 40P01. Основное исправление — брать объекты в одинаковом порядке. Редкая гонка всё равно возможна, поэтому безопасный повтор всей транзакции нужен и для 40P01.

#Очередь через SKIP LOCKED

Несколько обработчиков могут забирать разные задачи, пропуская уже занятые:

WITH next_job AS ( SELECT id FROM jobs WHERE status = 'pending' ORDER BY priority DESC, id FOR UPDATE SKIP LOCKED LIMIT 1 ) UPDATE jobs AS j SET status = 'running', locked_by = $1, locked_at = clock_timestamp() FROM next_job WHERE j.id = next_job.id RETURNING j.*;

SKIP LOCKED намеренно показывает неполный набор: занятые строки не ждутся, а пропускаются. Это правильно для очереди, но неправильно для полного отчёта.

Рабочая очередь также требует срока аренды задачи, повторов, идемпотентности, ограничения голодания и отдельного места для окончательно неудачных задач.

#Рекомендательные блокировки

Рекомендательная блокировка (advisory lock) связана не со строкой, а с числовым ключом, выбранным приложением:

SELECT pg_advisory_xact_lock(hashtextextended($1, 0));

PostgreSQL не знает смысл ключа. Блокировку соблюдают только участники одного соглашения. Транзакционный вариант освобождается при COMMIT или ROLLBACK. Сеансовый вариант живёт до явного освобождения или разрыва соединения и плохо сочетается с пулом транзакций.

Хеш может дать одинаковое число для разных бизнес-ключей. Это не нарушит данные, если результатом будет лишь лишняя блокировка, но увеличит задержку.

#Практика

В лаборатории 18 вы реализуете получение задачи из очереди. Отдельная проверка конкурентности запустит два настоящих сеанса и докажет, что они получили разные задачи.

Подробнее: уровни изоляции, явные блокировки, MVCC.

Далее: Как PostgreSQL хранит строки и WAL