Learning Platform
Урок 12.01 · 24 мин
Продвинутый
isolationANSI SQLanomaliesREAD COMMITTEDREPEATABLE READSERIALIZABLEsnapshot isolation

Модули 4 (MVCC) и 10 (блокировки) дали нам инструменты — версии строк, snapshot, row-level locks. Этот модуль про то, какие гарантии из этих инструментов складываются и как разработчику об этом думать. Начнём с базы: что говорит SQL-стандарт, какие аномалии он описывает и почему Postgres почти везде ведёт себя строже, чем обязан.

Четыре уровня по ANSI SQL-92

Стандарт SQL-92 определяет четыре уровня изоляции через разрешённые аномалии. Не через «как реализовано», а через «что может пойти не так»:

ANSI SQL-92: уровни изоляции и аномалии

Чем выше уровень, тем меньше аномалий разрешено. SERIALIZABLE — единственный уровень, обещающий результат, эквивалентный какому-то последовательному выполнению транзакций.

READ UNCOMMITTEDdirty / non-repeat / phantom
READ COMMITTEDnon-repeat / phantom
REPEATABLE READphantom
SERIALIZABLEничего
Логикакаждый следующий уровень запрещает ещё одну аномалию из списка предыдущего

Разберём аномалии:

  • Dirty read — T1 прочитала строку, которую T2 только что записала, но ещё не закоммитила. Если T2 потом сделает ROLLBACK, то T1 видела значение, которого в БД никогда не было.
  • Non-repeatable read — T1 прочитала одну и ту же строку дважды, и между чтениями T2 успела закоммитить UPDATE. Два значения в одной транзакции — разные.
  • Phantom read — T1 выполнила SELECT ... WHERE x > 10 дважды, и между этими SELECT’ами T2 вставила новую строку, удовлетворяющую условию. Множество строк изменилось.
  • Lost update (формально не входит в SQL-92, но описан в Berenson et al. 1995) — T1 и T2 одновременно делают read-modify-write одной строки, и одно из обновлений «теряется».

Berenson et al. (1995)
показали, что эта классификация неполная: реальные СУБД (Oracle, Postgres) используют snapshot isolation, который не вписывается в SQL-92 решётку и при этом допускает аномалию, которой в SQL-92 нет, — write skew. О ней — в уроке 4.

Что Postgres реально предоставляет

Postgres даёт четыре уровня по имени, но фактически только три различных:

Маппинг ANSI → реальное поведение Postgres

READ UNCOMMITTED в Postgres = READ COMMITTED (dirty read невозможен в MVCC). REPEATABLE READ = snapshot isolation. SERIALIZABLE с 9.1 — это SSI (Serializable Snapshot Isolation).

запросишь READ UNCOMMITTEDполучишь READ COMMITTED
запросишь READ COMMITTEDdefault; statement-snapshot
REPEATABLE READsnapshot isolation
SERIALIZABLESSI (с 9.1)

Ключевые отличия от стандарта:

  • READ UNCOMMITTED в Postgres невозможен физически. MVCC основан на snapshot’ах, видимость определяется через xmin/xmax. Грязное чтение требует увидеть кортеж с xmin от незакоммиченной транзакции — Postgres такие кортежи отфильтровывает на этапе visibility check. Поэтому SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED молча работает как READ COMMITTED.
  • REPEATABLE READ в Postgres сильнее стандарта: phantom read невозможен (snapshot фиксируется на первом statement и держится до конца транзакции). То, что ANSI называет REPEATABLE READ — это в Postgres snapshot isolation, который запрещает phantom, но допускает write skew.
  • SERIALIZABLE — это
    SSI
    : snapshot isolation + детектор циклов в графе зависимостей. О том, как это работает — уроки 3 и 4.

Текущий уровень транзакции

Уровень задаётся на уровне транзакции, а не сессии (хотя default_transaction_isolation устанавливает дефолт сессии):

Просмотр текущего уровня изоляции и default'а сессии. Перед BEGIN или внутри транзакции до первого запроса можно сменить уровень через SET TRANSACTION.

PostgreSQL

Сменить уровень для одной транзакции:

Переключение на REPEATABLE READ для конкретной транзакции. После COMMIT уровень вернётся к default.

PostgreSQL

Важно: SET TRANSACTION ISOLATION LEVEL действует, только если выполнен до первой команды транзакции. Иначе — ошибка SET TRANSACTION ISOLATION LEVEL must be called before any query.

Дорожки T1 / T2: как читать concurrency-диаграммы

В этом модуле мы будем часто рисовать две параллельные транзакции. Условимся о нотации:

Нотация: две дорожки во времени

Время идёт сверху вниз. Каждая строка — один statement. Колонка T1 слева, T2 справа. Пустая ячейка значит, что в этот момент эта транзакция ничего не делает.

T1BEGIN
T1SELECT balance FROM acc WHERE id=1
(ждёт)
T1UPDATE acc SET balance = ...
T1COMMIT
(ничего)
T2BEGIN
T2UPDATE acc SET balance = ...
(ждёт T1 / получает abort)
T2COMMIT or ERROR
реальное wall-clockвремя = строка сверху, синхронизация — пустыми ячейками

В pglite-песочнице, через которую мы работаем в этом курсе, одна connection — параллельных транзакций технически нет. Поэтому аномалии разбираем текстом + диаграммой, а в sandbox запускаем шаг-в-шаг изменения одной транзакции, чтобы проверить семантику изоляции.

Маленький эксперимент: non-repeatable read под READ COMMITTED

Покажем правую часть: что READ COMMITTED разрешает non-repeatable read. Здесь мы эмулируем «T2 закоммитила между двумя SELECT T1» через явный коммит подзапроса:

READ COMMITTED: каждый statement получает свой snapshot. Если бы между двумя SELECT в реальном мире другая транзакция сделала COMMIT — второй SELECT увидел бы новое значение. Здесь эмулируем серией коммитов.

PostgreSQL

А теперь то же самое, но в REPEATABLE READ:

REPEATABLE READ: snapshot фиксируется на первом statement. Все последующие чтения внутри транзакции видят то же состояние, даже если другие транзакции коммитят изменения.

PostgreSQL

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

Чего нет в Postgres

Несколько артефактов SQL-стандарта в Postgres не реализованы или работают «иначе»:

  • DEFERRABLE constraints внутри REPEATABLE READ — работают, но не пересекаются с моделью snapshot. Постановка SET CONSTRAINTS ALL DEFERRED откладывает проверку до COMMIT, и если в момент COMMIT’а констрейнт нарушен — транзакция падает.
  • Cursor stability (уровень из DB2) — отсутствует. Любой курсор внутри REPEATABLE READ-транзакции работает по snapshot, как обычный SELECT.
  • READ UNCOMMITTED — как уже сказали, физически невозможен. Postgres документирует это явно.

Что выбирать по умолчанию

В 95% production-приложений на Postgres — READ COMMITTED (default). Причины:

  • Минимальный overhead: snapshot пересчитывается на каждом statement, мёртвые tuple’ы виднее VACUUM’у быстрее.
  • Меньше serialization failures: ни SI, ни SSI не abort’ят транзакцию при конкуренции — просто видят более старое состояние или ждут lock.
  • Большинство ORM (Hibernate, Sequelize, ActiveRecord) предполагает RC: их retry-логика заточена под него.

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

SERIALIZABLE берут, когда в коде есть инварианты «прочитал-проверил-записал», которые легко сломать через write skew (модуль 4 урок 4). Цена — нужно научить приложение retry’ить транзакции с ошибкой 40001 serialization_failure.

В следующем уроке погружаемся в snapshot isolation — как именно Postgres решает, что видно, а что нет, и как через snapshot detect’ится write-write conflict.

Проверка знанийKnowledge check
Команда выставляет default_transaction_isolation = 'repeatable read' на проде, потому что «так безопаснее». Какие три практических последствия они получат?
ОтветAnswer
Во-первых, длинные REPEATABLE READ-транзакции держат snapshot всю свою жизнь, не давая VACUUM убирать dead tuples с xmax < OldestXmin — будет расти bloat. Во-вторых, любые конкурирующие UPDATE'ы одной строки получают ошибку 'could not serialize access due to concurrent update' (SQLSTATE 40001), и приложение должно уметь retry'ить — если нет, пользователи увидят случайные 500. В-третьих, write skew всё равно не лечится — REPEATABLE READ его допускает, нужно либо SERIALIZABLE, либо явные SELECT FOR UPDATE. То есть «безопаснее» — миф: получили операционные проблемы и не закрыли семантические.

Чек-лист

  • ANSI SQL-92 определяет 4 уровня через разрешённые аномалии: dirty read, non-repeatable read, phantom.
  • Berenson et al. (1995) добавили lost update и write skew — последний SQL-92 не описывает вовсе.
  • Postgres: READ UNCOMMITTED = READ COMMITTED (нет грязных чтений в MVCC). REPEATABLE READ = snapshot isolation. SERIALIZABLE = SSI.
  • Уровень устанавливается на транзакцию через BEGIN ISOLATION LEVEL X или SET TRANSACTION ISOLATION LEVEL X до первого statement.
  • Default — READ COMMITTED. Поднимать стоит точечно, с пониманием цены: bloat, serialization failures, retry-логика.
Четыре аномалии: dirty read, non-repeatable, phantom, write skew Уровни изоляции: что реально даёт PostgreSQL

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Запрос `SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED` в PostgreSQL — что произойдёт?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5