Уровни изоляции транзакций простыми словами
Зачем вообще нужна изоляция
Когда транзакции выполняются последовательно, одна за другой, проблем нет — каждая видит результат предыдущей и работает с консистентными данными. Но последовательное выполнение — это очередь, а очередь убивает пропускную способность. База хочет выполнять транзакции параллельно, а изоляция — это про то, насколько параллельные транзакции могут мешать друг другу и какие искажения данных мы готовы терпеть ради скорости.
Уровень изоляции — это не про то, что база умеет, а про то, какие аномалии она разрешает увидеть вашему коду. Чем строже уровень, тем меньше аномалий, но тем больше блокировок или конфликтов версий, и тем ниже throughput.
Аномалии, от слабой к сильной
- Dirty read — транзакция читает данные, которые другая транзакция ещё не закоммитила. Если та откатится, вы прочитали то, чего никогда не существовало.
- Non-repeatable read — в рамках одной транзакции повторное чтение той же строки даёт другой результат, потому что кто-то её изменил и закоммитил между вашими чтениями.
- Phantom read — то же самое, но на уровне диапазона: повторный запрос с тем же условием WHERE возвращает другой набор строк, потому что кто-то вставил или удалил подходящую запись.
Четыре уровня стандарта SQL
READ UNCOMMITTED — разрешены все три аномалии. На практике в большинстве СУБД (например, PostgreSQL) этот уровень фактически ведёт себя как READ COMMITTED — dirty read там не реализован, потому что грязных версий строк в MVCC-модели просто не отдают.
READ COMMITTED — самый частый уровень по умолчанию (Postgres, Oracle, SQL Server). Dirty read исключён: вы видите только закоммиченные данные. Но non-repeatable read и phantom остаются: каждый отдельный запрос внутри транзакции видит свежий снепшот на момент своего старта, а не на момент старта транзакции.
REPEATABLE READ — весь набор данных фиксируется на момент начала транзакции: повторное чтение строки всегда даёт тот же результат. По стандарту phantom read всё ещё возможен, но в PostgreSQL REPEATABLE READ реализован через снепшот всей транзакции целиком, и фантомы там фактически исключены — зато возможна ошибка serialization failure при конфликте записи.
SERIALIZABLE — эффект такой, как если бы транзакции выполнялись строго последовательно. Никаких аномалий. Цена — либо тяжёлые блокировки диапазонов, либо (в Postgres) SSI — Serializable Snapshot Isolation, которая детектит конфликты постфактум и откатывает одну из транзакций с ошибкой, которую надо ловить и ретраить.
Как это реализовано физически
Есть два подхода. Первый — блокировки (2PL, two-phase locking): читатель блокирует писателя, писатель блокирует читателя, и все ждут в очереди. Так исторически работал MySQL/InnoDB на старых уровнях и так работает SQL Server по умолчанию.
Второй — MVCC (multiversion concurrency control): каждая транзакция видит свой снепшот данных, читатели не блокируют писателей и наоборот. PostgreSQL, Oracle, InnoDB в MySQL — все построены на MVCC. Именно поэтому в Postgres нет отдельного READ UNCOMMITTED: физически невозможно прочитать незакоммиченную версию строки, снепшоты этого не позволяют.
Типичные ошибки в понимании
Отдельно стоит проблема lost update: две транзакции читают одно значение, обе увеличивают его на 1 и коммитят — в итоге значение выросло на 1, а не на 2. READ COMMITTED от этого не защищает, и её обычно решают либо через SELECT FOR UPDATE, либо через оптимистичную блокировку с version-полем.
Как отвечать на собеседовании
Хороший ответ — не пересказ таблицы аномалий, а демонстрация, что вы понимаете цену выбора. Скажите, какой уровень стоит по умолчанию в вашей базе, какие аномалии он допускает, и как вы бы решали конкретную проблему — например, race condition при обновлении баланса — не поднятием уровня изоляции глобально, а точечным блокированием строки.