Каким образом уровень READ COMMITTED допускает неповторяемое чтение, хотя не позволяет читать незакоммиченн...

Каким образом уровень READ COMMITTED допускает неповторяемое чтение, хотя не позволяет читать незакоммиченные данные?

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

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

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

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

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

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

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

Пусть транзакция дважды читает остаток товара или статус заказа. Между этими чтениями другая транзакция может изменить строку и зафиксировать результат.

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

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

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

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

Минимальная схема поведения выглядит так:

-- Транзакция T1 BEGIN; SELECT status FROM orders WHERE id = 10; -- T1 приостанавливается -- Транзакция T2 изменяет status и фиксирует транзакцию -- T1 продолжает работу SELECT status FROM orders WHERE id = 10; COMMIT;

Если T2 зафиксировала изменение между двумя запросами T1, результаты запросов могут различаться. При этом T1 не увидит незакоммиченное изменение T2 — именно это отличает READ COMMITTED от более слабой изоляции.

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

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

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

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

Рассматривались три варианта. Повторная проверка без изменения изоляции сохраняла окно гонки; REPEATABLE READ давал согласованное чтение, но требовал анализа возможных конфликтов обновления; блокировка строки на время всей операции уменьшала риск изменения заказа, но увеличивала время удержания блокировки и могла ухудшить параллелизм.

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

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

  1. Всегда ли READ COMMITTED использует блокировки для чтения?

Нет. В одних СУБД чтение использует краткоживущие разделяемые блокировки, в других — версионный механизм MVCC, а конкретное поведение может зависеть от настроек. Общий эффект уровня — отсутствие чтения незакоммиченных данных, но внутренний способ его достижения не универсален.

  1. Может ли повторное чтение при READ COMMITTED вернуть прежнее значение после изменения строки?

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

  1. Достаточно ли повысить изоляцию, чтобы безопасно проверить условие и изменить данные?

Не всегда. Повторяемое чтение может стабилизировать видимые версии уже найденных строк, но не обязательно защищает весь диапазон от появления новых строк; это зависит от уровня и реализации СУБД. Для проверки и изменения бизнес-инварианта нужны подходящий уровень изоляции, блокировка диапазона, атомарное условное изменение или другой явно выбранный механизм защиты.