Программирование SQLТранзакции и конкурентный доступМладший разработчик серверных приложений, работающий с реляционными СУБД

Во время формирования отчёта транзакция видит изменение, которое другая транзакция ещё не зафиксировала, по...

Во время формирования отчёта транзакция видит изменение, которое другая транзакция ещё не зафиксировала, после чего это изменение откатывается. Какой риск конкурентного доступа здесь проявляется и каким механизмом изоляции он допускается?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Это грязное чтение: транзакция использует данные, которые ещё не стали окончательными и могут исчезнуть после отката другой транзакции. Такой риск допускает уровень READ UNCOMMITTED; уровни READ COMMITTED и выше не должны возвращать незакоммиченные изменения.

Исторический контекст

Транзакции одновременно решают две противоположные задачи: обеспечивают корректность данных и позволяют нескольким клиентам работать параллельно. Полная изоляция, близкая к последовательному выполнению, обычно требует большего числа блокировок, ожиданий или версий данных.

Поэтому в SQL появились уровни изоляции как компромисс между согласованностью и производительностью. READ UNCOMMITTED минимизирует ожидания чтения, но допускает использование промежуточного состояния данных.

Постановка проблемы

Предположим, одна транзакция временно уменьшила баланс счёта, но ещё не завершилась. Если отчёт прочитает это значение, а затем исходная транзакция откатится, отчёт будет основан на факте, которого в базе окончательно не существует.

Последствия зависят от задачи: неверные финансовые итоги, ложные уведомления, некорректные решения о доступности товара или нарушение бизнес-инвариантов. Особенно опасно, что ошибка может исчезнуть при повторном чтении и плохо воспроизводиться.

Подробное решение

При READ UNCOMMITTED чтение может увидеть незакоммиченные изменения другой транзакции. СУБД не требует дождаться фиксации записи и не ограничивает читателя только подтверждённой версией данных.

Минимальный сценарий выглядит так:

-- Сессия 1 BEGIN; UPDATE accounts SET balance = 0 WHERE id = 1; -- Изменение ещё не зафиксировано -- Сессия 2, при реально поддерживаемом READ UNCOMMITTED SELECT balance FROM accounts WHERE id = 1; -- Сессия 1 ROLLBACK;

Если вторая сессия увидела нулевой баланс, она прочитала промежуточное состояние. После ROLLBACK в базе останется прежнее значение, поэтому результат чтения нельзя считать надёжным.

READ COMMITTED устраняет именно эту проблему: читатель видит только зафиксированные данные. Реализация может использовать блокировки или MVCC-снимки; важен результат — незакоммиченная версия не возвращается.

Однако READ COMMITTED не делает чтение полностью стабильным. Между двумя операторами другая транзакция может зафиксировать изменение, поэтому повторное чтение способно вернуть другое значение. Для более строгих гарантий применяют более высокий уровень изоляции, блокировки или явное проектирование транзакции.

Поведение уровня READ UNCOMMITTED зависит от СУБД. Некоторые системы формально принимают этот уровень, но фактически предоставляют более строгую изоляцию и не показывают грязные данные. Поэтому при проектировании нужно проверять документацию конкретного движка, а не полагаться только на название уровня.

Ситуация из практики

Сервис формирует отчёт о доступном остатке товара во время массового пересчёта складских резервов. Пересчёт сначала записывает промежуточные значения, а затем либо фиксирует все изменения, либо откатывает транзакцию при ошибке.

Вариант с READ UNCOMMITTED уменьшает ожидание отчёта, но отчёт может показать товар как доступный или недоступный на основании данных, которые впоследствии исчезнут. Это неприемлемо, если результат используется для оформления заказа.

Вариант с блокировками на чтение делает результат надёжнее, но может задержать отчёты и увеличить конкуренцию за строки. Вариант с READ COMMITTED обычно позволяет читать последнюю зафиксированную картину без грязных данных и без длительной блокировки записи.

Разумное решение — использовать READ COMMITTED для отчёта, а для операции оформления заказа повторно проверить остаток внутри собственной транзакции с подходящей защитой от конкурентного изменения. Отчёт тогда остаётся информативным, а окончательное бизнес-решение не основывается на промежуточных данных.

Что кандидаты часто упускают

  1. Обязательно ли грязное чтение означает чтение значения, которое потом будет изменено?

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

  1. Почему обычное ожидание блокировки может предотвращать грязное чтение?

Если запись защищена блокировкой до завершения изменяющей транзакции, читатель не получает её промежуточное состояние. Он ждёт фиксации или отката: после фиксации видит новое значение, после отката — старое.

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

  1. Достаточно ли перейти с READ UNCOMMITTED на READ COMMITTED, чтобы отчёт всегда был согласованным целиком?

Нет. READ COMMITTED запрещает грязные чтения, но обычно действует на уровне отдельного оператора. Если отчёт выполняет несколько запросов, между ними другие транзакции могут зафиксировать изменения, и части отчёта будут отражать разные моменты времени.

Для единого снимка нескольких чтений нужен соответствующий режим согласованного снимка, более строгий уровень изоляции или другая архитектура формирования отчёта. Выбор зависит от допустимой задержки, нагрузки и требований к согласованности.