Перейти к содержанию

Уровни изоляции PostgreSQL

Атомарность говорит о совместной фиксации команд, но не определяет всё, что читатель увидит при конкурентной работе. Редактор может начать отчёт, другой клиент изменить цену, а затем первый продолжит читать каталог. Уровень изоляции задаёт правила видимости такого изменения. В PostgreSQL одинаковые названия уровней нельзя без проверки понимать по историческому курсу другой СУБД.

Используем PostgreSQL 17.11 и прежний курс js-browser: цена 1990, версия один. Снимок catalog-db-advanced/lesson-18 находится в архиве продолжения. SQL не запускался. Для будущего ручного наблюдения нужны две живые сессии одной учебной базы; шаги загружаются в них через \i, а не отдельными завершающимися процессами psql -f.

Снимок чтения

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

Начнём первый опыт после отдельного reset и убедимся, что других учебных транзакций нет. В сессии A выполняется read-committed/A1.sql:

BEGIN ISOLATION LEVEL READ COMMITTED READ ONLY;
SELECT price, version FROM catalog.courses WHERE id = 2;

Ожидаются 1990 и один. Оставьте эту же сессию открытой. В сессии B затем выполните B1.sql:

BEGIN;
UPDATE catalog.courses
SET price = 2090, version = version + 1
WHERE id = 2;
COMMIT;

После завершения B вернитесь в A и загрузите A2.sql:

SELECT price, version FROM catalog.courses WHERE id = 2;
COMMIT;

Ожидаются 2090 и два. A всё ещё находилась в одной транзакции, но её вторая команда началась после чужой фиксации. Неповторяемое чтение здесь является допустимым свойством выбранного уровня, а не потерей первого ответа в памяти клиента. Правила разобраны в документации изоляции PostgreSQL 17.

Если B пока не выполнила COMMIT, обычный повтор SELECT в A не должен прочитать её незавершённую цену. Порядок действий существенен. Таблица шагов в README специально отделяет «команда отправлена» от «транзакция завершена», чтобы ожидаемые числа не зависели от догадки о том, что успел второй клиент.

Repeatable Read

Закончите обе сессии и отдельно восстановите тот же исходный снимок. Нельзя начинать второй опыт с уже сохранённой ценой 2090 и ожидать прежние числа. Теперь A выполняет вариант repeatable-read/A1.sql:

BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
SELECT price, version FROM catalog.courses WHERE id = 2;

После первого чтения ожидаются 1990 и один. B выполняет то же изменение с COMMIT. Второе чтение A в пределах этого блока должно по-прежнему показать 1990 и один. После завершения блока новый SELECT увидит 2090 и два. Снимок A остаётся согласованным с её начальным наблюдением, а не означает отмену изменения B.

Важно, что BEGIN и первый запрос — разные моменты. Для этого уровня PostgreSQL фиксирует снимок при первой подходящей команде транзакции, а не предлагает универсальное обещание состояния в ту секунду, когда человек напечатал BEGIN. В подготовленном A1 сразу следует SELECT, поэтому в опыте эта граница видна без дополнительной неопределённости.

Read Only сообщает намерение не изменять данные. Он не является отдельным уровнем изоляции и не заменяет Repeatable Read. В Read Committed Read Only два чтения всё ещё могут различаться. Поэтому название режима следует читать как два независимых свойства: что операция может делать и какое представление данных использует.

Повторяемость не равна всем предметным гарантиям

Стабильное чтение одной строки полезно для отчёта, но не доказывает согласованность любой конкурентной бизнес-операции. Два клиента могут прочитать разные части общего условия и изменить разные строки. Отсутствие непосредственного конфликта одного ключа ещё не означает, что оба результата совместимы с предметным правилом.

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

Название Read Uncommitted в PostgreSQL не даёт чтение незавершённых чужих строк: оно работает как Read Committed. Это хороший пример, почему списку уровней из стандарта недостаточно доверять без конкретной реализации. В старом уроке SQL Server изучалась другая СУБД, её детали не становятся автоматически поведением PostgreSQL.

Изменение требует своего разбора

В этой главе A только читает. Если превратить её в редактора, который затем пытается обновить уже изменённую строку, появится другой сценарий. На более строгом уровне такая попытка может завершиться ошибкой, а при Read Committed нужно понимать повторную проверку условий и актуальную строку. Нельзя переносить ожидаемый результат двух SELECT на произвольную последовательность UPDATE без анализа.

Так же нельзя считать продолжительность снимка бесплатной. Долго открытая транзакция сохраняет потребность в старом представлении и влияет на сопровождение версий строк. Для короткой карточки читателя обычно не нужна многочасовая Repeatable Read-сессия. Для цельного отчёта границу выбирают осознанно и заканчивают после получения необходимых данных.

Один отчёт и несколько отдельных запросов

Представим страницу редактора, которая сначала выводит число опубликованных курсов, а затем отдельным SELECT получает их список. Между запросами другой клиент публикует черновик. На Read Committed оба чтения могут быть корректными для своих моментов, но число и список на одном экране будут относиться к разным состояниям. Это не грязное чтение: в обоих случаях прочитаны подтверждённые данные.

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

У Repeatable Read сохранённый снимок удобен для согласованного чтения, но не делает открытый экран актуальным бесконечно. Пока отчёт строится, редактор в другой сессии может сохранить новую цену. Первый читатель вправе видеть старую цену в пределах своего снимка; после завершения блока новое чтение получит другую актуальность. Интерфейс должен понимать, показывает ли он состояние отчёта или текущую карточку.

На этом месте полезно разнести два требования: «все части ответа согласованы между собой» и «ответ учитывает последнее подтверждённое действие другого человека». Одно не гарантирует другое. В нашем опыте одинаковые запросы A дают разные результаты именно потому, что мы меняем правило видимости, сохраняя учебную цену и порядок шагов. Так причина различия становится наблюдаемой, а не скрывается за общим словом «конкуренция».

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

Порядок первого опыта таков: A начинает и читает, B меняет и фиксирует, A повторяет чтение и завершает. Второй опыт повторяет этот порядок после reset с другим уровнем. Файлы в разных папках не предназначены для одновременного запуска целиком: так вы потеряете управляемую точку наблюдения.

Если одна сессия закрылась после A1, её транзакция уже не продолжается. Новый клиент получит новое соединение и не сможет показать обещанный старый снимок. Поэтому инструкцию \i нужно выполнять в тех же двух открытых окнах psql. При ошибке сначала используйте ROLLBACK текущего блока; cleanup.sql предназначен именно для этого.

При будущей ручной сверке отметьте три значения: первая цена A, подтверждённая цена B и вторая цена A. Для Read Committed это 1990, 2090, 2090; для Repeatable Read — 1990, 2090, 1990 внутри блока. Теперь различие объясняется выбранной границей видимости, а не случайной скоростью переключения между окнами.

Оглавление курса.