Уровни изоляции транзакций
| Стандарт | SQL-92 — четыре уровня |
|---|---|
| Уровни | Read Uncommitted, Read Committed, Repeatable Read, Serializable |
| Аномалии стандарта | грязное чтение, неповторяющееся чтение, фантомы |
| Дополнительные аномалии | потерянное обновление, перекос записи (write skew) |
| Критика стандарта | Беренсон и др. (1995) |
| По умолчанию | Read Committed (PostgreSQL, Oracle, SQL Server), Repeatable Read (MySQL InnoDB) |
Уровень изоляции транзакций — настройка СУБД, определяющая, насколько параллельно выполняющиеся транзакции изолированы друг от друга: видит ли одна транзакция незафиксированные или только что зафиксированные изменения другой. Полная изоляция — результат как при последовательном выполнении — дорога, поэтому стандарт SQL определяет несколько уровней, на которых ради производительности допускаются те или иные аномалии.
Уровень изоляции — буква «I» в ACID и частый источник тонких ошибок: код, правильно работающий в тестах с одним пользователем, под нагрузкой теряет обновления или нарушает бизнес-правила.
Аномалии
| Аномалия | Что происходит |
|---|---|
| Грязное чтение (dirty read) | транзакция видит изменения другой, ещё не зафиксированные (и, возможно, отменённые) |
| Неповторяющееся чтение (non-repeatable read) | повторное чтение той же строки даёт другое значение — её изменили и зафиксировали |
| Фантомы (phantom read) | повторный запрос с тем же условием возвращает новые строки |
| Потерянное обновление (lost update) | две транзакции читают значение, меняют его и записывают — одно изменение теряется |
| Перекос записи (write skew) | две транзакции читают общие данные, принимают решения и меняют разные строки, вместе нарушая правило |
Классический пример перекоса записи: в больнице должен дежурить хотя бы один врач. Два дежурных одновременно проверяют, что дежурных двое, и каждый снимает себя с дежурства. Каждая транзакция по отдельности корректна, а вместе они оставили больницу без врача.
Уровни стандарта
| Уровень | Грязное чтение | Неповторяющееся чтение | Фантомы |
|---|---|---|---|
| Read Uncommitted | возможно | возможно | возможны |
| Read Committed | нет | возможно | возможны |
| Repeatable Read | нет | нет | возможны |
| Serializable | нет | нет | нет |
Уже в 1995 году Беренсон и соавторы показали, что определения стандарта неполны и неоднозначны: они не учитывают потерянные обновления и перекос записи и не описывают широко используемую изоляцию снимков (snapshot isolation), при которой транзакция видит согласованный снимок базы на момент начала.[1] Поэтому реальные СУБД под одинаковыми названиями дают разные гарантии.
Как это реализовано
PostgreSQL
Благодаря MVCC читатели никогда не блокируют писателей.[2]
- Read Committed (по умолчанию) — каждый оператор видит данные, зафиксированные к моменту его начала. Грязного чтения нет, но два
SELECTв одной транзакции могут увидеть разное. - Repeatable Read — изоляция снимков: вся транзакция видит один снимок. При попытке изменить строку, которую уже изменила параллельная транзакция, возникает ошибка сериализации. Фантомов нет, но перекос записи возможен.
- Serializable — сериализуемая изоляция снимков (SSI, с версии 9.1): поверх снимков СУБД отслеживает опасные зависимости чтения и записи и прерывает одну из транзакций, если результат не мог бы получиться при последовательном выполнении.[3]
- Read Uncommitted ведёт себя как Read Committed.
Другие СУБД
- MySQL InnoDB по умолчанию использует Repeatable Read: обычные чтения идут по снимку, а блокирующие чтения и изменения — с блокировками промежутков индекса (gap locks), предотвращающими фантомы.[4]
- Oracle по умолчанию — Read Committed; его уровень Serializable на деле является изоляцией снимков.
- SQLite выполняет записывающие транзакции по одной, поэтому фактически сериализуема.
Сравнение гарантий разных систем и формальные модели согласованности собраны проектом Jepsen.[5]
Как выбирать и что делать
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT count(*) FROM doctors WHERE on_call AND shift_id = 42;
UPDATE doctors SET on_call = false WHERE id = 7 AND shift_id = 42;
COMMIT; -- может завершиться ошибкой 40001: could not serialize access
- Read Committed подходит для большинства запросов, если изменения выражены атомарно:
UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100вместо «прочитать в приложении — посчитать — записать». - Явные блокировки для критичных мест:
SELECT ... FOR UPDATEблокирует прочитанные строки до конца транзакции и предотвращает потерянное обновление. - Ограничения целостности —
UNIQUE,CHECK, исключающие ограничения — защищают инварианты на любом уровне изоляции. - Serializable — когда правила затрагивают несколько строк и их трудно выразить ограничениями. Приложение обязано уметь повторять транзакцию при ошибке сериализации (код 40001) — это нормальная часть работы, а не авария.
- Держите транзакции короткими: чем дольше транзакция, тем выше вероятность конфликтов.
Подробный разбор аномалий с примерами — в книге Мартина Клеппмана «Высоконагруженные приложения».[6]
См. также
Источники
- H. Berenson et al. — A Critique of ANSI SQL Isolation Levels (SIGMOD 1995)
- PostgreSQL Documentation — Transaction Isolation
- D. Ports, K. Grittner — Serializable Snapshot Isolation in PostgreSQL (VLDB 2012)
- MySQL — Transaction Isolation Levels (InnoDB)
- Jepsen — Consistency Models
- M. Kleppmann — Designing Data-Intensive Applications