Уровни изоляции
Уровень изоляции — настройка, которая определяет, какие «помехи» от параллельных транзакций ваша транзакция готова терпеть ради скорости.
Уровень изоляции — компромисс между строгостью и производительностью: чем выше уровень, тем меньше аномалий вы видите, но тем дороже обходится параллельная работа.
Буква «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 требует повтора при ошибке сериализации.