Практическая ситуация: при READ COMMITTED отчёт читает балансы двух счетов отдельными операторами, пока другая транзакция переводит деньги между ними. Почему отчёт может увидеть комбинацию значений, которой не существовало в базе одновременно?
CREATE TABLE accounts (
id integer PRIMARY KEY,
balance integer NOT NULL
);
INSERT INTO accounts VALUES (1, 100), (2, 100);
-- Сессия 1: отчёт
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT balance FROM accounts WHERE id = 1;
-- Сессия 2: перевод
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
UPDATE accounts SET balance = balance + 50 WHERE id = 2;
COMMIT;
-- Сессия 1 продолжает отчёт
SELECT balance FROM accounts WHERE id = 2;
COMMIT;
Это аномалия чтения с разбросом: одна транзакция получает согласованное состояние для первого оператора, затем — уже другое состояние для второго. При типичной реализации READ COMMITTED снимок создаётся заново для каждого оператора, поэтому отчёт может увидеть старый баланс одного счёта и новый баланс другого.
Чтобы все операторы транзакции читали один снимок, применяют REPEATABLE READ или более строгий SERIALIZABLE. Точное поведение зависит от СУБД и её реализации уровня изоляции.
Уровни изоляции появились как компромисс между полной последовательностью выполнения транзакций и производительностью конкурентного доступа. Полная изоляция упрощает рассуждение о данных, но может приводить к большему числу блокировок, ожиданий и откатов.
READ COMMITTED уменьшает вероятность чтения неподтверждённых данных, позволяя транзакциям видеть новые подтверждённые изменения между отдельными операторами. Такой компромисс удобен для коротких операций, но недостаточен для многошагового чтения, которому требуется единый снимок.
В примере первоначальная сумма счетов равна 200. Если перевод выполнен между двумя запросами отчёта, первый запрос может вернуть 100, а второй — 150; отчёт вычислит сумму 250, хотя такая сумма никогда не существовала в базе.
Проблема не в нарушении атомарности перевода: сама транзакция перевода либо фиксируется целиком, либо откатывается. Нарушается согласованность наблюдения: читатель комбинирует результаты, относящиеся к разным моментам времени.
В реализации, основанной на версионности, каждый оператор при READ COMMITTED обычно читает данные, подтверждённые к началу именно этого оператора. Первый SELECT видит состояние до перевода. После COMMIT второй SELECT получает новый снимок и видит состояние после перевода.
Если оба значения нужно получить согласованно, можно выполнить их одним оператором:
Один оператор в типичной MVCC-СУБД использует один снимок, поэтому результаты относятся к одному моменту чтения. Это не означает, что вся последующая работа транзакции будет читать тот же снимок.
REPEATABLE READ обычно закрепляет снимок за всей транзакцией. Читатель получает согласованное представление данных, но может столкнуться с конфликтом при попытке записи или с повышенными требованиями к хранению версий.
SERIALIZABLE обеспечивает поведение, эквивалентное некоторому последовательному порядку транзакций. СУБД может блокировать операции или завершить транзакцию ошибкой сериализации, поэтому приложение должно уметь повторять всю транзакцию.
Следует отличать согласованное чтение от гарантии бизнес-инвариантов. Единый снимок предотвращает разброс чтения, но сам по себе не делает все конкурентные действия эквивалентными последовательному выполнению.
Финансовый отчёт читает десятки таблиц несколькими запросами. При READ COMMITTED отдельные результаты могут относиться к разным моментам, поэтому итоговые суммы не обязаны сходиться.
Вариант с одним большим запросом уменьшает окно несогласованности и часто эффективен, но усложняет SQL и не всегда подходит для сложной логики отчёта. Вариант с REPEATABLE READ проще для многошагового чтения, однако длительная транзакция может удерживать старые версии и повысить нагрузку на очистку данных.
SERIALIZABLE даёт наиболее строгую гарантию, но способен чаще вызывать ожидания и ошибки сериализации. Для отчёта, который только читает данные и допускает повторный запуск, обычно выбирают REPEATABLE READ либо специальный согласованный снимок, а для проверки критичного инварианта — SERIALIZABLE с корректным повтором транзакции.
Нет. Это как раз типичное отличие READ COMMITTED от REPEATABLE READ в MVCC-СУБД: снимок создаётся для каждого оператора. Однако конкретные детали видимости и блокировок зависят от продукта, поэтому при ответе на собеседовании важно называть СУБД, если требуется её точное поведение.
Для чтения, выполняемого одним оператором, в типичной MVCC-реализации да: оператор использует единый снимок. Но это решение не защищает последующие операторы транзакции от изменений, которые будут зафиксированы позже, и не заменяет SERIALIZABLE для конкурентной проверки инвариантов.
Не обязательно. В некоторых СУБД этот уровень основан на snapshot isolation: транзакция видит стабильный снимок, но независимые транзакции всё ещё могут совместно нарушить бизнес-правило, если каждая проверяет данные, а затем изменяет другой объект. Для гарантии эквивалентности последовательному выполнению нужен SERIALIZABLE либо явная синхронизация доступа.