ЯдроКодаподготовка к экзаменам
Научная библиотека

Загружаем научный разбор

Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.

Каталог статейМатериал и источники

PostgreSQL: почему снимок данных не гарантирует согласованность решения

Автор: · Обновлено

MVCC, аномалия write skew и Serializable Snapshot Isolation: разбор двух транзакций, инварианта и правильной границы повтора.

В расписании всегда должен оставаться хотя бы один дежурный. Сейчас дежурят Анна и Борис. Каждый открывает приложение, видит второго дежурного и снимает себя со смены. Обе операции по отдельности выглядят допустимыми, но после их совместного выполнения дежурных не остаётся. Ошибка касается общего правила, хотя пользователи меняли разные строки.

Такой сценарий называют write skew. Он помогает понять, почему слова «транзакция» и «снимок» не являются синонимами полной согласованности бизнес-решения. База может правильно защитить отдельные записи и одновременно разрешить историю, несовместимую с последовательным выполнением всей проверки и изменения.

Что даёт снимок

MVCC позволяет читать подходящие версии строк, уменьшая прямую конкуренцию читателей и писателей. В PostgreSQL уровень Repeatable Read обеспечивает стабильный снимок для транзакции, однако допускает аномалии сериализации. Serializable требует, чтобы успешно завершённые транзакции имели результат, совместимый с некоторым последовательным порядком. Для этого часть конкурентных транзакций может завершаться ошибкой сериализации. Документация PostgreSQL 18.

Работа Ports и Grittner, VLDB 2012 объясняет Serializable Snapshot Isolation в PostgreSQL: система отслеживает зависимости между транзакциями и предотвращает опасные сочетания, сохраняя преимущества чтения снимков. Здесь важна гарантия результата, а не представление, будто все запросы буквально выстраиваются в одну физическую очередь. Первичная статья.

Собственный сценарий двух сеансов

Для воспроизведения используйте отдельную учебную базу. Создайте таблицу ровно с двумя строками. Затем откройте два SQL-сеанса. Сначала в обоих выполните начало транзакции и SELECT, только после этого переходите к UPDATE и COMMIT. Порядок действий существенен: последовательный запуск целых сценариев не создаст нужного пересечения.

CREATE TABLE duty_demo (
    name text PRIMARY KEY,
    on_call boolean NOT NULL
);
INSERT INTO duty_demo VALUES ('anna', true), ('boris', true);

-- Сеанс A: выполнить до UPDATE сеанса B.
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) FROM duty_demo WHERE on_call; -- 2

-- Сеанс B: выполнить до UPDATE сеанса A.
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) FROM duty_demo WHERE on_call; -- 2

-- Сеанс A, после обеих проверок:
UPDATE duty_demo SET on_call = false WHERE name = 'anna';
COMMIT;

-- Сеанс B, после обеих проверок:
UPDATE duty_demo SET on_call = false WHERE name = 'boris';
COMMIT;

Комментарии обозначают распределение команд между двумя соединениями. Их нельзя просто вставить целиком в одно окно: второй BEGIN внутри первой транзакции не создаст независимого сеанса. После выполнения обеих транзакций отдельный SELECT показывает ноль дежурных. Снимки были внутренне последовательны, но решения опирались на состояние, которое другая транзакция одновременно меняла.

Повторите опыт на заново заполненной таблице, заменив уровень изоляции в обоих сеансах на SERIALIZABLE. Одновременно зафиксировать обе операции при таком пересечении не должно получиться: одна из транзакций получит ошибку сериализации. Не привязывайте приложение к тому, какой именно сеанс окажется отменённым и на какой команде обнаружится конфликт.

Повторять нужно всё решение

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

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

Альтернативой может быть явная сериализация через общую строку расписания и блокировку этой строки. Тогда все изменения дежурств одной смены используют один протокол. Просто блокировать только собственную строку недостаточно: Анна и Борис вновь возьмут разные блокировки. Выбор зависит от нагрузки, структуры инварианта и того, какие операции обязаны участвовать в соглашении.

Границы вывода и рабочая проверка

Не каждый инвариант требует такого сложного протокола. Уникальность конкретного ключа лучше выражать ограничением базы, если оно соответствует предметному правилу. Приложение тогда обрабатывает конфликт как понятный исход операции. Наше правило о наличии хотя бы одного дежурного зависит от набора строк и потому требует анализа совместного поведения. Отдельно проверьте административные скрипты, фоновые задания и импорты: если они меняют расписание в обход выбранного соглашения, корректность веб-обработчика не сохранит общий инвариант системы.

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

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

Источники

Формат и права

Формат
Авторский разбор

Атрибуция

Самостоятельный русскоязычный разбор ЯдроКода. Описания первоисточников отделены от авторских учебных примеров и инженерных выводов. Материал не является переводом или перепечаткой.

Код, данные и иллюстрации

Учебные данные, расчёты, таблицы и программные примеры созданы для этой публикации. Иллюстрации и программный код из первоисточников не воспроизводятся.