Уровни изоляции определяют, насколько параллельные транзакции видят изменения друг друга. Это один из самых частых вопросов на senior-интервью по базам данных.
Четыре проблемы конкурентного доступа
Прежде чем изучать уровни изоляции, нужно понять проблемы, которые они решают.
1. Dirty Read (Грязное чтение)
Транзакция читает незакоммиченные изменения другой транзакции.
Timeline:
T1: BEGIN;
T1: UPDATE products SET price = 0 WHERE id = 5; -- not committed yet
T2: SELECT price FROM products WHERE id = 5; -- returns 0 (dirty!)
T1: ROLLBACK; -- T1 cancelled
-- T2 made decisions based on wrong price
Опасность: T2 приняла решение (например, добавила товар в корзину по цене 0₽) на основе данных, которые не существуют.
2. Non-Repeatable Read (Неповторяемое чтение)
Повторное чтение той же строки в одной транзакции возвращает разные данные.
T1: BEGIN;
T1: SELECT stock FROM products WHERE id = 5; -- returns 10
T2: UPDATE products SET stock = 3 WHERE id = 5; COMMIT;
T1: SELECT stock FROM products WHERE id = 5; -- returns 3 (different!)
T1: -- Makes decision based on stock=3, but started with stock=10
Опасность: логика внутри T1 может стать некорректной, потому что данные изменились по ходу её выполнения.
3. Phantom Read (Фантомное чтение)
Повторный запрос возвращает разный набор строк из-за INSERT/DELETE в другой транзакции.
T1: BEGIN;
T1: SELECT COUNT(*) FROM orders WHERE user_id = 42; -- returns 3
T2: INSERT INTO orders (user_id, total) VALUES (42, 1000); COMMIT;
T1: SELECT COUNT(*) FROM orders WHERE user_id = 42; -- returns 4 (phantom!)
Отличие от Non-Repeatable Read: изменяется не содержимое строки, а набор строк.
4. Serialization Anomaly (Аномалия сериализации)
Результат параллельных транзакций невозможно получить при любом последовательном выполнении.
-- T1: перевести все положительные балансы в premium
-- T2: перевести всех premium-клиентов обратно в regular
-- Параллельное выполнение может дать состояние, которое не соответствует
-- ни T1→T2, ни T2→T1
Четыре уровня изоляции
READ UNCOMMITTED
Самый слабый уровень. Транзакция может читать незакоммиченные изменения.
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
-- Can see uncommitted changes from other transactions
SELECT * FROM accounts; -- may return dirty data
COMMIT;
| Проблема | Защита |
|---|---|
| Dirty Read | ✗ нет |
| Non-Repeatable Read | ✗ нет |
| Phantom Read | ✗ нет |
| Serialization Anomaly | ✗ нет |
Когда используется: практически никогда в production. PostgreSQL технически не поддерживает этот уровень — обрабатывает его как READ COMMITTED.
READ COMMITTED
Транзакция видит только закоммиченные данные, но данные могут меняться между чтениями.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- default in PostgreSQL
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- returns committed value
-- Other transaction commits a change here
SELECT balance FROM accounts WHERE id = 1; -- may return different value!
COMMIT;
| Проблема | Защита |
|---|---|
| Dirty Read | ✓ да |
| Non-Repeatable Read | ✗ нет |
| Phantom Read | ✗ нет |
| Serialization Anomaly | ✗ нет |
Применение: уровень по умолчанию в PostgreSQL и Oracle. Подходит для большинства CRUD-операций, где нет зависимостей между несколькими чтениями.
Реализация в PostgreSQL: каждый SELECT видит снимок данных на момент начала конкретного запроса (а не транзакции).
REPEATABLE READ
Данные, прочитанные в начале транзакции, не изменятся до её завершения. Но новые строки могут появляться.
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- returns 1000
-- Other transaction: UPDATE accounts SET balance = 500 WHERE id = 1; COMMIT;
SELECT balance FROM accounts WHERE id = 1; -- still returns 1000 (snapshot!)
-- Other transaction: INSERT INTO accounts VALUES (99, 999); COMMIT;
SELECT COUNT(*) FROM accounts; -- may see new row (phantom)
COMMIT;
| Проблема | Защита |
|---|---|
| Dirty Read | ✓ да |
| Non-Repeatable Read | ✓ да |
| Phantom Read | ✗ нет (в SQL standard) / ✓ (в PostgreSQL) |
| Serialization Anomaly | ✗ нет |
Важный нюанс PostgreSQL: в PostgreSQL REPEATABLE READ реализован через MVCC и снимок на момент начала транзакции — это защищает и от Phantom Read. Это отличается от стандарта SQL.
Реализация: транзакция работает со снимком данных (snapshot), сделанным в момент первого запроса транзакции.
SERIALIZABLE
Самый строгий уровень. Параллельные транзакции дают тот же результат, что и любое их последовательное выполнение.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- If conflicts detected with another SERIALIZABLE transaction,
-- one of them will get: ERROR: could not serialize access due to concurrent update
-- Application must retry the transaction
COMMIT;
| Проблема | Защита |
|---|---|
| Dirty Read | ✓ да |
| Non-Repeatable Read | ✓ да |
| Phantom Read | ✓ да |
| Serialization Anomaly | ✓ да |
Реализация в PostgreSQL: SSI (Serializable Snapshot Isolation) — отслеживание зависимостей между транзакциями, откат при конфликте. Менее агрессивен, чем блокировочный подход.
Цена: возможны ошибки could not serialize access — приложение обязано повторять транзакцию при таких ошибках.
Сводная таблица
| Уровень | Dirty Read | Non-Repeatable | Phantom | По умолчанию в |
|---|---|---|---|---|
| READ UNCOMMITTED | ✗ | ✗ | ✗ | — (нигде) |
| READ COMMITTED | ✓ | ✗ | ✗ | PostgreSQL, Oracle |
| REPEATABLE READ | ✓ | ✓ | ✗* | MySQL InnoDB |
| SERIALIZABLE | ✓ | ✓ | ✓ | — |
*В PostgreSQL REPEATABLE READ также защищает от Phantom Read.
MVCC — механизм за кулисами
PostgreSQL реализует изоляцию через MVCC (Multi-Version Concurrency Control) — хранение нескольких версий каждой строки.
Как работает MVCC
Row versions in PostgreSQL:
| xmin | xmax | data |
|-------|-------|--------------|
| 100 | 0 | balance=1000 | -- created by tx 100, not deleted
| 100 | 150 | balance=1000 | -- deleted/updated by tx 150
| 150 | 0 | balance=500 | -- new version by tx 150
xmin— ID транзакции, создавшей версиюxmax— ID транзакции, удалившей/обновившей версию (0 = активна)
При SELECT каждая транзакция видит только версии строк, видимые в её снимке (snapshot).
Преимущества MVCC
- Читатели не блокируют писателей — конкурентность без взаимного ожидания
- Писатели не блокируют читателей — SELECT не ждёт UPDATE
Недостатки MVCC
- Bloat (раздувание) — старые версии строк накапливаются
- VACUUM — процесс очистки устаревших версий, критичен для производительности
-- Check table bloat
SELECT schemaname, tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as total_size,
n_dead_tup as dead_tuples
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
Как выбрать уровень изоляции
Алгоритм выбора
1. Нужна ли защита от Dirty Read?
Да → используй READ COMMITTED или выше
Нет (аналитика, логи) → READ UNCOMMITTED (если СУБД поддерживает)
2. Читаешь данные несколько раз в одной транзакции?
Нет → READ COMMITTED достаточно
Да → нужен REPEATABLE READ или SERIALIZABLE
3. Делаешь агрегации или INSERT по результату SELECT?
Да → нужен SERIALIZABLE
4. Критична ли согласованность выше производительности?
Да → SERIALIZABLE
Нет → READ COMMITTED
Примеры по задачам
| Задача | Уровень | Причина |
|---|---|---|
| Показ статьи из блога | READ COMMITTED | Один запрос, данные не критичны |
| Оформление заказа | READ COMMITTED / REPEATABLE READ | Проверка наличия товара |
| Банковский перевод | SERIALIZABLE | Нужна строгая согласованность |
| Генерация отчёта | REPEATABLE READ | Данные не должны меняться во время генерации |
| Онлайн-аналитика | READ COMMITTED | Небольшая неточность допустима |
Примеры для интервью
Вопрос: «Что выберешь для финансовых операций?»
Стандартный ответ: «SERIALIZABLE, но с обработкой serialization errors в приложении и повторными попытками (retry). Альтернатива — использовать SELECT FOR UPDATE для явных блокировок на нужном уровне READ COMMITTED.»
Вопрос: «Почему PostgreSQL по умолчанию READ COMMITTED, а не SERIALIZABLE?»
Ответ: «Производительность. SERIALIZABLE создаёт накладные расходы на отслеживание зависимостей и может приводить к ошибкам сериализации, требующим повторной попытки. Большинство запросов — простые SELECT/INSERT без конкурентных зависимостей, для них READ COMMITTED достаточно.»
Вопрос: «Как PostgreSQL защищает от Phantom Read на уровне REPEATABLE READ?»
Ответ: «MVCC — транзакция работает со снимком данных на момент начала первой операции. INSERT другой транзакции после этого момента просто невидим для текущей транзакции. Это сильнее гарантий SQL-стандарта.»