Уровни изоляции
Уровни изоляции транзакций — баланс между целостностью данных при параллельном доступе и производительностью. READ UNCOMMITTED (самый слабый): допускает грязное чтение (чтение незафиксированных изменений), максимальная скорость. READ COMMITTED (умолчание в PostgreSQL, Oracle, SQL Server): защищает от грязного чтения, допускает неповторяющееся чтение и фантомы. REPEATABLE READ (умолчание в MySQL InnoDB): защищает от грязного чтения и неповторяющегося чтения, фантомы возможны (кроме PostgreSQL и MySQL InnoDB, где защищают). SERIALIZABLE (самый строгий): защищает от всех аномалий (грязное чтение, неповторяющееся чтение, фантомы), требует повторных попыток транзакций при конфликтах (SSI в PostgreSQL). Сравнение уровней, особенности в разных СУБД (PostgreSQL, MySQL InnoDB, SQL Server, Oracle), примеры, типичные ошибки (использование READ UNCOMMITTED по умолчанию, игнорирование ошибок сериализации).
Введение: Проблема параллельного доступа
Представьте, что вы и ваш друг одновременно пытаетесь забронировать последнее место в кинотеатре через сайт. Вы открываете страницу, видите, что место свободно, и нажимаете “Забронировать”. Друг делает то же самое в ту же секунду. Кто получит место?
Если система работает правильно, место достанется только одному из вас. Но если система не управляет параллельным доступом должным образом, возможны разные сценарии: вы оба увидите, что место свободно, оба нажмете “Забронировать”, и система либо дважды продаст одно место (плохо), либо откажет одному из вас с непонятной ошибкой (тоже плохо, но лучше чем двойная продажа).
В базах данных параллельные транзакции создают похожие проблемы. Транзакции могут читать данные, которые другая транзакция еще не зафиксировала. Могут видеть разные версии одних и тех же данных в рамках одного запроса. Могут терять изменения, сделанные параллельно.
Уровни изоляции — это механизм, который позволяет разработчику выбирать баланс между целостностью данных и производительностью. Чем выше уровень изоляции, тем больше защита от проблем параллельного доступа, но тем ниже производительность и выше вероятность блокировок.
Что такое уровень изоляции
Уровень изоляции (Isolation Level) — это набор правил, определяющих, насколько изменения, сделанные одной транзакцией, видны другим транзакциям до ее фиксации.
Уровень изоляции отвечает на вопросы:
- Может ли транзакция видеть незафиксированные изменения другой транзакции?
- Если транзакция читает одни и те же данные дважды, гарантированно ли она увидит одинаковые значения?
- Могут ли появиться новые строки в диапазоне, который транзакция уже прочитала?
Стандарт SQL определяет четыре уровня изоляции (от самого слабого к самому сильному):
| Уровень изоляции | Что защищает | Какие аномалии допускает |
|---|---|---|
| READ UNCOMMITTED | Почти ничего | Грязное чтение, неповторяющееся чтение, фантомы |
| READ COMMITTED | Грязное чтение | Неповторяющееся чтение, фантомы |
| REPEATABLE READ | Грязное чтение, неповторяющееся чтение | Фантомы |
| SERIALIZABLE | Все перечисленные аномалии | Никаких |
Эти уровни образуют иерархию: каждый следующий уровень включает защиту предыдущего. SERIALIZABLE — самый строгий, READ UNCOMMITTED — самый слабый.
Но важно понимать: стандарт SQL определяет минимальные гарантии для каждого уровня. Конкретные СУБД могут предоставлять более сильные гарантии, чем требует стандарт. Например, PostgreSQL на уровне REPEATABLE READ защищает и от фантомов, хотя стандарт этого не требует.
Грязное чтение (Dirty Read)
Чтобы понять уровни изоляции, нужно сначала понять аномалии, от которых они защищают. Первая и самая опасная аномалия — грязное чтение. (Остальные аномалии подробно раскрываются в следующем документе темы “Транзакции”, здесь дается минимальное определение.)
Грязное чтение — это ситуация, когда одна транзакция читает данные, которые были изменены другой транзакцией, но еще не зафиксированы.
Пример:
| Время | Транзакция A (перевод) | Транзакция B (отчет) |
|---|---|---|
| t1 | BEGIN | |
| t2 | UPDATE accounts SET balance = balance - 100 WHERE id = 1 | |
| t3 | SELECT balance FROM accounts WHERE id = 1 | |
| t4 | → Видит сумму уже после списания (но транзакция A еще не зафиксирована!) | |
| t5 | ROLLBACK (ошибка, отмена перевода) | |
| t6 | Отчет использовал данные, которых никогда не существовало |
Транзакция B прочитала сумму, которую транзакция A в итоге откатила. Отчет оказался неверным, основанным на данных-призраках.
Грязное чтение — это самая грубая аномалия, которую допускает только уровень READ UNCOMMITTED. Все остальные уровни защищают от нее.
READ UNCOMMITTED: Самый быстрый, но самый опасный
Что это такое
READ UNCOMMITTED — самый низкий (самый слабый) уровень изоляции. Транзакция на этом уровне может читать незафиксированные изменения других транзакций. Ее еще называют “грязное чтение” (dirty read) по имени основной аномалии.
Какие гарантии дает
READ UNCOMMITTED дает почти никаких гарантий:
- Допускает грязное чтение.
- Допускает неповторяющееся чтение.
- Допускает фантомы.
- Не ставит блокировок при чтении.
Когда используется
READ UNCOMMITTED используется крайне редко в production-системах. Его единственное преимущество — максимальная скорость чтения, потому что транзакция не ждет блокировок и не тратит ресурсы на контроль версий.
Типичные сценарии:
| Сценарий | Применимость |
|---|---|
| Приблизительные отчеты | Отчет, где небольшая неточность допустима, а скорость критична |
| Мониторинг и наблюдение | Слежение за ходом выполнения длительных операций |
| Очень большие базы данных для чтения | Где загрузка настолько высока, что блокировки недопустимы |
| Тестирование и отладка | Чтобы увидеть, что делает параллельная транзакция |
Важно: В PostgreSQL READ UNCOMMITTED не поддерживается. Если вы попытаетесь его установить, PostgreSQL будет вести себя как READ COMMITTED. Это сделано намеренно — разработчики PostgreSQL считают грязное чтение слишком опасной аномалией, чтобы разрешать ее даже по запросу.
Пример работы
-- Сессия 1 (транзакция A)
BEGIN;
UPDATE products SET price = 0 WHERE id = 1; -- Цена стала 0, но еще не зафиксировано
-- Сессия 2 (транзакция B)
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT price FROM products WHERE id = 1; -- Вернет 0 (грязное чтение!)
-- Сессия 1
ROLLBACK; -- Откатываем изменение, цена была 100 и осталась 100
-- Транзакция B использовала данные, которых никогда не существовалоREAD COMMITTED: Золотая середина по умолчанию
Что это такое
READ COMMITTED — самый распространенный уровень изоляции. Он используется по умолчанию в PostgreSQL, Oracle, SQL Server (в режиме READ_COMMITTED_SNAPSHOT = OFF) и многих других СУБД.
Транзакция на этом уровне видит только зафиксированные данные. Грязное чтение исключено. Но другие аномалии (неповторяющееся чтение, фантомы) возможны.
Какие гарантии дает
READ COMMITTED гарантирует:
- Нет грязного чтения. Вы никогда не увидите незафиксированные изменения другой транзакции.
READ COMMITTED НЕ гарантирует:
- Неповторяющееся чтение. Если вы прочитаете одну и ту же строку дважды в рамках одной транзакции, вы можете получить разные значения, если другая транзакция изменила и зафиксировала эту строку между вашими чтениями.
- Фантомы. Если вы сделаете выборку по условию, а потом повторите ту же выборку, могут появиться новые строки, удовлетворяющие условию.
Как работает под капотом
В большинстве СУБД READ COMMITTED реализуется через комбинацию блокировок и многоверсионности (MVCC).
В СУБД с MVCC (PostgreSQL, Oracle):
- Каждый запрос видит снимок данных (snapshot) на момент начала выполнения этого запроса.
- Разные запросы внутри одной транзакции могут видеть разные снимки, если между ними были изменения от других транзакций.
В СУБД с блокировками (SQL Server без SNAPSHOT):
- При чтении ставятся кратковременные блокировки, которые снимаются сразу после чтения.
- Другие транзакции могут изменять данные между чтениями.
Пример неповторяющегося чтения
-- Сессия 1 (транзакция A)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- Видит 1000
-- Сессия 2 (транзакция B)
BEGIN;
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT; -- Изменение зафиксировано
-- Сессия 1
SELECT balance FROM accounts WHERE id = 1; -- Видит 900 (неповторяющееся чтение!)
COMMIT;Транзакция A дважды прочитала одну и ту же строку и получила разные значения. Для многих приложений это приемлемо, но не для всех.
Когда используется READ COMMITTED
READ COMMITTED — выбор по умолчанию для большинства OLTP-систем.
| Сценарий | Почему подходит |
|---|---|
| Большинство веб-приложений | Высокая производительность, достаточная защита от грязных чтений |
| Системы, где неповторяющееся чтение не критично | Например, если вы не делаете несколько чтений одной строки в транзакции |
| Системы с высокой конкуренцией | Меньше блокировок, чем на более высоких уровнях |
READ COMMITTED в разных СУБД
| СУБД | Особенности READ COMMITTED |
|---|---|
| PostgreSQL | READ COMMITTED по умолчанию. Каждый запрос видит снимок на момент начала запроса. |
| Oracle | READ COMMITTED по умолчанию. Аналогично PostgreSQL, использует версионирование. |
| MySQL (InnoDB) | REPEATABLE READ по умолчанию, но READ COMMITTED поддерживается. |
| SQL Server | READ COMMITTED по умолчанию, но есть два режима: с блокировками (по умолчанию) и SNAPSHOT. |
REPEATABLE READ: Стабильный взгляд на данные
Что это такое
REPEATABLE READ — уровень изоляции, который гарантирует, что если транзакция прочитала строку один раз, повторное чтение той же строки в рамках той же транзакции вернет то же самое значение. Неповторяющееся чтение исключено.
Этот уровень используется по умолчанию в MySQL (InnoDB).
Какие гарантии дает
REPEATABLE READ гарантирует:
- Нет грязного чтения.
- Нет неповторяющегося чтения. Если вы прочитали строку, никто не сможет изменить ее так, чтобы вы увидели другое значение при повторном чтении.
REPEATABLE READ НЕ гарантирует (по стандарту SQL):
- Фантомы. Другая транзакция может вставить новые строки, удовлетворяющие условию вашей выборки. При повторной выборке эти строки появятся.
Важное исключение: Некоторые СУБД (например, PostgreSQL, MySQL InnoDB в режиме REPEATABLE READ) защищают и от фантомов на практике, хотя стандарт этого не требует.
Как работает под капотом
REPEATABLE READ обычно реализуется через многоверсионность (MVCC) с более строгими правилами, чем READ COMMITTED.
В PostgreSQL и Oracle:
- Транзакция видит снимок данных на момент начала транзакции, а не на момент каждого запроса.
- Все запросы внутри транзакции видят одну и ту же версию данных.
В MySQL InnoDB:
- Используется многоверсионность с gap-блокировками для защиты от фантомов.
- Gap-блокировки блокируют не только существующие строки, но и промежутки между ними.
Пример (отсутствие неповторяющегося чтения)
-- Сессия 1 (транзакция A)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- Видит 1000
-- Сессия 2 (транзакция B)
BEGIN;
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT; -- Изменение зафиксировано
-- Сессия 1
SELECT balance FROM accounts WHERE id = 1; -- Все еще видит 1000 (защита от неповторяющегося чтения!)
COMMIT;Транзакция A продолжает видеть старую версию данных, даже после того как транзакция B зафиксировала изменение.
Когда используется REPEATABLE READ
| Сценарий | Почему подходит |
|---|---|
| Системы, где важна согласованность в рамках транзакции | Например, формирование отчета на основе нескольких запросов |
| Балансы и финансовые операции | Повторное чтение должно давать тот же результат |
| MySQL-приложения | Поскольку это уровень по умолчанию в InnoDB |
SERIALIZABLE: Полная изоляция
Что это такое
SERIALIZABLE — самый высокий (самый строгий) уровень изоляции. Он гарантирует, что параллельно выполняющиеся транзакции дают тот же результат, как если бы они выполнялись последовательно, одна за другой.
SERIALIZABLE защищает от всех аномалий: грязного чтения, неповторяющегося чтения, фантомов и других.
Какие гарантии дает
SERIALIZABLE гарантирует:
- Нет грязного чтения.
- Нет неповторяющегося чтения.
- Нет фантомов.
- Линеаризуемость — транзакции упорядочены так, как будто они выполнялись последовательно.
Как работает под капотом
SERIALIZABLE может быть реализован разными способами:
Через блокировки (ранние СУБД):
- Ставятся блокировки на все читаемые и изменяемые данные.
- Блокировки диапазонов (range locks) защищают от фантомов.
- Высокий риск взаимных блокировок (deadlocks) и низкая производительность.
Через многоверсионность с проверкой конфликтов (PostgreSQL, современные СУБД):
- Транзакции работают со своими версиями данных (как в REPEATABLE READ).
- При фиксации (COMMIT) проверяется, не было ли конфликтов с другими транзакциями.
- Если конфликт обнаружен (например, одна транзакция изменила данные, которые читала другая), транзакция откатывается с ошибкой “cannot serialize”.
Этот подход называется Serializable Snapshot Isolation (SSI). Он дает лучшую производительность, чем блокировки, но требует, чтобы приложение было готово повторять транзакции, которые откатились из-за конфликта.
Пример (защита от фантомов)
-- Сессия 1 (транзакция A)
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
SELECT COUNT(*) FROM products WHERE category = 'books'; -- Получили 10
-- Сессия 2 (транзакция B)
BEGIN;
INSERT INTO products (category) VALUES ('books');
COMMIT;
-- Сессия 1
SELECT COUNT(*) FROM products WHERE category = 'books'; -- Все еще 10 (защита от фантомов)
COMMIT;Если бы уровень изоляции был REPEATABLE READ, транзакция A могла бы увидеть новую книгу при повторном чтении (фантом). SERIALIZABLE этого не допускает.
Цена SERIALIZABLE
SERIALIZABLE — самый дорогой уровень изоляции с точки зрения производительности.
В системах с блокировками:
- Высокая вероятность взаимных блокировок (deadlocks).
- Низкая пропускная способность.
- Длинные очереди ожидания.
В системах с SSI (PostgreSQL):
- Более высокая пропускная способность, чем с блокировками.
- Но транзакции могут откатываться с ошибкой “could not serialize access” при конфликтах.
- Приложение должно уметь повторять такие транзакции.
Когда используется SERIALIZABLE
SERIALIZABLE используется редко, только когда целостность данных критичнее производительности.
| Сценарий | Почему нужен SERIALIZABLE |
|---|---|
| Финансовые проводки с жесткими требованиями | Никакие аномалии недопустимы |
| Системы бронирования уникальных ресурсов | Двойное бронирование недопустимо |
| Инвентаризация и складской учет | Точность важнее скорости |
| Аналитические запросы, требующие абсолютной согласованности | Отчет должен видеть непротиворечивый снимок |
Сравнение уровней изоляции в таблице
| Характеристика | READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE |
|---|---|---|---|---|
| Грязное чтение | Возможно | Невозможно | Невозможно | Невозможно |
| Неповторяющееся чтение | Возможно | Возможно | Невозможно | Невозможно |
| Фантомы | Возможны | Возможны | Возможны (кроме некоторых СУБД) | Невозможны |
| Скорость чтения | Максимальная | Высокая | Средняя | Низкая |
| Скорость записи | Высокая | Средняя | Средняя | Низкая |
| Вероятность блокировок | Низкая | Средняя | Средняя | Высокая |
| Вероятность deadlock | Низкая | Средняя | Средняя | Высокая |
| Требует повтор транзакций | Нет | Нет | Нет | Да (при SSI) |
| Умолчание в PostgreSQL | Не поддерживается | Да | Нет | Нет |
| Умолчание в MySQL InnoDB | Нет | Нет | Да | Нет |
| Умолчание в Oracle | Нет | Да | Нет | Нет |
| Умолчание в SQL Server | Нет | Да | Нет | Нет |
Уровни изоляции в разных СУБД: Нюансы и отличия
Стандарт SQL определяет минимальные требования к каждому уровню изоляции. Но конкретные СУБД часто предоставляют более сильные гарантии, чем требует стандарт.
PostgreSQL
| Уровень | Особенности в PostgreSQL |
|---|---|
| READ UNCOMMITTED | Не поддерживается, ведет себя как READ COMMITTED |
| READ COMMITTED | Уровень по умолчанию. Каждый запрос видит снимок на момент начала запроса |
| REPEATABLE READ | Защищает от фантомов (сильнее, чем требует стандарт). Транзакция видит снимок на момент начала транзакции |
| SERIALIZABLE | Реализован через SSI. Может откатывать транзакции с ошибкой сериализации |
MySQL (InnoDB)
| Уровень | Особенности в MySQL InnoDB |
|---|---|
| READ UNCOMMITTED | Поддерживается, но не рекомендуется |
| READ COMMITTED | Каждый запрос видит зафиксированные изменения. Возможны неповторяющиеся чтения |
| REPEATABLE READ | Уровень по умолчанию. Защищает от фантомов через gap-блокировки |
| SERIALIZABLE | Блокирует все читаемые строки (SELECT … LOCK IN SHARE MODE). Очень низкая производительность |
SQL Server
| Уровень | Особенности в SQL Server |
|---|---|
| READ UNCOMMITTED | Поддерживается, также есть NOLOCK hint |
| READ COMMITTED | Уровень по умолчанию. Может работать в двух режимах: с блокировками (по умолчанию) и READ COMMITTED SNAPSHOT (включение версионирования) |
| REPEATABLE READ | Блокирует все прочитанные строки до конца транзакции |
| SERIALIZABLE | Блокирует диапазоны, защищая от фантомов |
Oracle
| Уровень | Особенности в Oracle |
|---|---|
| READ UNCOMMITTED | Не поддерживается |
| READ COMMITTED | Уровень по умолчанию. Каждый запрос видит только зафиксированные данные |
| REPEATABLE READ | Явно не поддерживается. Достигается через READ ONLY транзакции |
| SERIALIZABLE | Поддерживается. Транзакция видит снимок на момент начала |
Как выбрать уровень изоляции. Примеры
| Если ваш сценарий… | Рекомендуемый уровень |
|---|---|
| Большинство веб-приложений, интернет-магазины | READ COMMITTED |
| Приложения на MySQL | REPEATABLE READ (это умолчание) |
| Финансовые транзакции с высокими требованиями | REPEATABLE READ или SERIALIZABLE |
| Приблизительные отчеты, мониторинг | READ COMMITTED (или даже READ UNCOMMITTED, если очень критична скорость) |
| Аналитические запросы к OLTP-базе | READ COMMITTED |
| Бронирование уникальных ресурсов (билеты, места) | REPEATABLE READ или SERIALIZABLE |
| Системы, где любая аномалия недопустима | SERIALIZABLE |
Когда можно использовать READ UNCOMMITTED
Почти никогда в production-системах с критичными данными. Единственные сценарии:
- Мониторинг и наблюдение за ходом выполнения длительных операций.
- Очень большие отчеты, где небольшая неточность допустима.
- Тестовые среды.
Предупреждение: READ UNCOMMITTED может читать данные, которые будут откачены. Если вы используете его для принятия бизнес-решений, вы рискуете.
Когда стоит подумать о повышении уровня изоляции
Признаки того, что READ COMMITTED может быть недостаточно:
- Вы делаете несколько чтений внутри одной транзакции и ожидаете, что данные не изменятся.
- Вы формируете отчет, который должен быть согласован на момент начала формирования.
- Вы реализуете логику, которая зависит от того, что значение не изменилось между чтением и обновлением (оптимистичная блокировка).
Когда SERIALIZABLE избыточен
SERIALIZABLE — это тяжелая артиллерия. Не используйте его, если:
- Ваше приложение может работать с неповторяющимися чтениями.
- У вас высокая нагрузка на запись.
- Вы не готовы обрабатывать ошибки сериализации (повтор транзакций).
Типичные ошибки при работе с уровнями изоляции
Ошибка 1: Использовать READ UNCOMMITTED по умолчанию
Некоторые разработчики думают: “Мне нужна максимальная скорость, поставлю READ UNCOMMITTED”. Это может привести к тому, что приложение будет читать данные, которые никогда не существовали.
Как исправить: Используйте READ UNCOMMITTED только тогда, когда вы полностью осознаете риски и готовы их принять.
Ошибка 2: Думать, что более высокий уровень изоляции всегда лучше
SERIALIZABLE — это не “лучше”, это “строже”. Он решает одни проблемы, но создает другие (блокировки, deadlocks, ошибки сериализации).
Как исправить: Выбирайте минимальный уровень изоляции, который обеспечивает требования вашего приложения.
Ошибка 3: Не учитывать поведение конкретной СУБД
“REPEATABLE READ в MySQL защищает от фантомов, а в PostgreSQL — тоже защищает, хотя стандарт этого не требует”. Хорошо. Но в других СУБД может быть иначе.
Как исправить: Знайте, как ваша СУБД реализует каждый уровень изоляции. Не полагайтесь только на стандарт.
Ошибка 4: Не обрабатывать ошибки сериализации
При использовании SERIALIZABLE в PostgreSQL (и других СУБД с SSI) транзакции могут откатываться с ошибкой “could not serialize access”. Если приложение не умеет повторять такие транзакции, пользователи будут видеть необъяснимые ошибки.
Как исправить: В коде, использующем SERIALIZABLE, всегда предусматривайте повтор транзакции при ошибке сериализации.
Ошибка 5: Путать уровень изоляции с механизмом изоляции
Уровень изоляции (READ COMMITTED, REPEATABLE READ) — это “что”. Механизм изоляции (блокировки, MVCC, SSI) — это “как”. Одно и то же уровень может быть реализован по-разному в разных СУБД.
Как исправить: Понимайте разницу. При анализе производительности важно знать не только уровень, но и механизм.
Резюме для системного аналитика
Уровень изоляции определяет баланс между целостностью данных при параллельном доступе и производительностью. Чем выше уровень, тем больше защиты, но ниже производительность.
READ UNCOMMITTED — самый слабый уровень. Допускает грязное чтение. Используется крайне редко, только когда скорость важнее точности.
READ COMMITTED — самый распространенный уровень, умолчание в PostgreSQL, Oracle, SQL Server. Защищает от грязного чтения, но допускает неповторяющееся чтение и фантомы.
REPEATABLE READ — уровень, который гарантирует стабильность прочитанных строк. Защищает от грязного чтения и неповторяющегося чтения. Умолчание в MySQL InnoDB.
SERIALIZABLE — самый строгий уровень. Защищает от всех аномалий, включая фантомы. Требует повторной попытки транзакций при конфликтах (в современных реализациях).
Одна и та же СУБД может реализовывать уровни изоляции по-разному. Знайте особенности вашей СУБД. Не полагайтесь только на стандарт.