ГлавнаяБлог › Уровни изоляции транзакций простыми словами

Уровни изоляции транзакций простыми словами

Зачем вообще нужна изоляция

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

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

Представь read-only копию Google Docs, которая делается в момент открытия документа. READ COMMITTED — это как обновлять копию перед каждым новым действием. REPEATABLE READ — сделал одну копию в начале и работаешь с ней до конца, не важно, что происходит в оригинале. SERIALIZABLE — плюс к этому система следит, чтобы твои изменения не конфликтовали с чужими, и если конфликт есть — просит переделать с нуля.

Типичные ошибки в понимании

Частая путаница: думать, что более строгий уровень изоляции — это всегда просадка по производительности из-за блокировок. В MVCC-системах строгий уровень (SERIALIZABLE через SSI) не блокирует чтение вообще, но добавляет шанс получить ошибку сериализации на коммите — и приложение обязано уметь ретраить транзакцию. Забыть про retry-логику — типичная причина продовых инцидентов после смены уровня изоляции.
Вторая ошибка — считать, что уровень изоляции задаётся один раз глобально и забыть о нём. На практике разные транзакции в одном сервисе могут требовать разных уровней: отчётный запрос на REPEATABLE READ, а горячая запись баланса — на SERIALIZABLE или с явным SELECT FOR UPDATE, потому что дефолтный READ COMMITTED допускает lost update.

Отдельно стоит проблема lost update: две транзакции читают одно значение, обе увеличивают его на 1 и коммитят — в итоге значение выросло на 1, а не на 2. READ COMMITTED от этого не защищает, и её обычно решают либо через SELECT FOR UPDATE, либо через оптимистичную блокировку с version-полем.

Как отвечать на собеседовании

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

Чем REPEATABLE READ в PostgreSQL отличается от REPEATABLE READ по стандарту SQL?
По стандарту REPEATABLE READ допускает phantom read. В Postgres он реализован через снепшот всей транзакции на MVCC, поэтому фантомы фактически не возникают — зато конкурентная запись может привести к ошибке serialization failure, которую нужно обрабатывать retry.
Может ли READ COMMITTED привести к потере данных при инкременте счётчика?
Да, это классический lost update: обе транзакции читают старое значение, обе пишут +1, итоговый результат меньше ожидаемого. Решается через SELECT FOR UPDATE, атомарный UPDATE ... SET x = x + 1 или optimistic locking с version-полем.

Ещё разборы