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

Уровень изоляции — настройка, которая определяет, какие «помехи» от параллельных транзакций ваша транзакция готова терпеть ради скорости.

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

Буква «I» в ACID обещает изоляцию, но в реальной СУБД полная изоляция дорога. Поэтому стандарт SQL определяет четыре уровня: вы сами выбираете, какие странности при одновременной работе допустимы. На загруженном сервере это выбор между «быстро, но иногда увижу чужую кашу» и «всё корректно, но транзакции толкаются локтями».

В предыдущем уроке мы разобрали транзакцию в одиночку. Теперь — что происходит, когда их много и они работают с одними и теми же строками одновременно.

Зачем это на практике

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

Три классические аномалии

Сначала разберём «болезни», от которых лечат уровни изоляции. Все они возникают, когда две транзакции, T1 и T2, идут параллельно.

Грязное чтение (dirty read)

T1 читает строку, которую T2 изменила, но ещё не закоммитила. Если T2 потом сделает ROLLBACK, T1 поработала с данными, которых никогда официально не существовало.

T2: BEGIN; UPDATE accounts SET balance = 1000000 WHERE id = 1;  -- не COMMIT!
T1: BEGIN; SELECT balance FROM accounts WHERE id = 1;  -- видит 1000000 (грязь)
T2: ROLLBACK;   -- баланс никогда не был миллионом, а T1 уже на него рассчитала

Неповторяемое чтение (non-repeatable read)

T1 читает одну и ту же строку дважды и получает разные значения, потому что между чтениями T2 успела изменить эту строку и закоммитить.

T1: BEGIN; SELECT balance FROM accounts WHERE id = 1;  -- 500
T2: BEGIN; UPDATE accounts SET balance = 800 WHERE id = 1; COMMIT;
T1: SELECT balance FROM accounts WHERE id = 1;  -- уже 800, хотя T1 не закончилась

Фантомное чтение (phantom read)

T1 дважды выполняет один и тот же запрос с условием, и во второй раз появляются новые строки («фантомы») — T2 вставила или удалила строки, попадающие под условие.

T1: BEGIN; SELECT count(*) FROM accounts WHERE balance > 100;  -- 3 строки
T2: BEGIN; INSERT INTO accounts VALUES (99, 'Ева', 500); COMMIT;
T1: SELECT count(*) FROM accounts WHERE balance > 100;  -- уже 4 — появился фантом

Разница между неповторяемым и фантомным чтением: первое — про изменение существующих строк, второе — про появление/исчезновение строк, подходящих под условие WHERE.

Четыре уровня и что они запрещают

Уровень задаётся командой SET TRANSACTION ISOLATION LEVEL .... Стандарт описывает, какие аномалии на каждом уровне гарантированно невозможны.

УровеньГрязное чтениеНеповторяемое чтениеФантомы
READ UNCOMMITTEDвозможновозможновозможно
READ COMMITTEDнетвозможновозможно
REPEATABLE READнетнетвозможно*
SERIALIZABLEнетнетнет

«возможно» означает «стандарт это разрешает», а не «обязательно случится». Конкретная СУБД может быть строже стандарта (звёздочка у фантомов — про это ниже).

-- синтаксис установки уровня (перед началом работы транзакции)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
  SELECT ...;
COMMIT;

READ UNCOMMITTED и READ COMMITTED

READ UNCOMMITTED — самый слабый: разрешено всё, включая грязное чтение. На практике почти не используется (а в PostgreSQL он вообще ведёт себя как READ COMMITTED). READ COMMITTED — рабочая лошадка и уровень по умолчанию во многих СУБД: вы никогда не видите незакоммиченных чужих изменений, но в рамках одной транзакции повторный SELECT может выдать другой результат.

REPEATABLE READ и SERIALIZABLE

REPEATABLE READ гарантирует, что прочитанные строки не «уплывут»: внутри транзакции вы видите согласованный снимок данных. SERIALIZABLE — самый строгий: результат любого набора параллельных транзакций эквивалентен какому-то их выполнению строго по очереди. Никаких аномалий, но цена — конфликты сериализации и откаты.

Как это работает под капотом

Звёздочка в таблице важна: реальные СУБД часто строже стандарта. В PostgreSQL уровень REPEATABLE READ реализован через снимок (snapshot) и не допускает фантомов при чтении — он видит данные такими, какими они были на момент первого запроса транзакции. А PostgreSQL SERIALIZABLE использует механизм SSI (Serializable Snapshot Isolation): он отслеживает зависимости между транзакциями и, обнаружив цикл, способный нарушить сериализуемость, откатывает одну из транзакций с ошибкой could not serialize access.

Практический вывод: на SERIALIZABLE приложение обязано уметь повторить транзакцию, получившую ошибку сериализации. Это нормальный сценарий, а не сбой: СУБД честно сообщает «ваши транзакции конфликтуют, попробуйте ещё раз».

Частые ошибки

  • Считать уровень по умолчанию «достаточным» вслепую. READ COMMITTED отлично подходит большинству задач, но для денежных операций «прочитал баланс → принял решение → списал» он допускает гонку: между чтением и записью значение могло измениться.
  • Думать, что SERIALIZABLE «просто медленнее, но всегда работает». Он может отклонить транзакцию с ошибкой сериализации — без логики повтора приложение начнёт падать под нагрузкой.
  • Путать неповторяемое чтение и фантомы. Из-за этого выбирают слишком слабый или слишком сильный уровень. Запомните: неповторяемое — про изменение строк, фантомы — про новые строки под условие.
  • Менять уровень глобально, чтобы «починить один баг». Это бьёт по всем запросам. Поднимайте изоляцию точечно для конкретной транзакции.

Итоги

  • Изоляция — это компромисс: выше уровень → меньше аномалий, но дороже параллельная работа.
  • Три аномалии: грязное чтение (чужое незакоммиченное), неповторяемое чтение (строка изменилась), фантомы (появились/исчезли строки под условие).
  • READ UNCOMMITTED → READ COMMITTED → REPEATABLE READ → SERIALIZABLE — каждый следующий запрещает на одну аномалию больше.
  • READ COMMITTED — типичный дефолт; SERIALIZABLE даёт полную корректность ценой возможных откатов.
  • Реальные СУБД бывают строже стандарта: PostgreSQL REPEATABLE READ не пускает фантомов, а SERIALIZABLE требует повтора при ошибке сериализации.
Проверьте себя
1. Транзакция дважды читает одну и ту же строку и получает разные значения, потому что другая транзакция изменила её и закоммитила между чтениями. Как называется эта аномалия?
AГрязное чтение (dirty read)
BНеповторяемое чтение (non-repeatable read)
CФантомное чтение (phantom read)
DПотерянное обновление (lost update)
2. Какой уровень изоляции по стандарту SQL запрещает грязное чтение, но допускает неповторяемое чтение и фантомы?
AREAD UNCOMMITTED
BREAD COMMITTED
CREPEATABLE READ
DSERIALIZABLE
3. Что приложение обязано уметь делать при работе на уровне SERIALIZABLE в PostgreSQL?
AИгнорировать ошибки — они никогда не возникают на этом уровне
BПовторять транзакцию при ошибке сериализации
CВручную ставить блокировки на все читаемые строки
DОтключать автокоммит для каждого SELECT