Практическая ситуация: две транзакции при уровне READ COMMITTED читают одну строку, каждая вычисляет новое значение на основе прочитанного, затем сохраняет его. Как возникает потеря обновления?
Потеря обновления возникает, когда обе транзакции читают одно исходное значение, независимо вычисляют результат, а затем последовательно перезаписывают строку. Последняя запись затирает эффект первой, поэтому итог отражает только одну операцию вместо двух.
Уровень READ COMMITTED обычно не запрещает такой сценарий: он защищает чтение от уже зафиксированных изменений других транзакций, но не резервирует прочитанное значение до момента последующего обновления.
Конкурентное выполнение транзакций появилось как способ одновременно обслуживать множество запросов без полного последовательного выполнения. Полная сериализация давала простое поведение, но резко ограничивала пропускную способность и увеличивала время ожидания.
Поэтому СУБД используют блокировки, версии строк или их сочетание. Главная исходная задача этих механизмов — сохранить требуемые свойства ACID, особенно согласованность данных и изоляцию, не исключая параллелизм полностью.
Пусть на счёте находится 100 единиц. Две транзакции читают это значение: первая собирается вычесть 30, вторая — вычесть 20. Обе получают 100, затем первая записывает 70, а вторая — 80.
После фиксации останется 80, хотя математически ожидаемый результат равен 50. Одна подтверждённая операция потеряна, а база данных может оставаться формально непротиворечивой с точки зрения типов и ограничений, что делает ошибку особенно опасной.
Механизм состоит из четырёх шагов: чтение исходного значения, вычисление в приложении, запись вычисленного абсолютного значения, фиксация. Между чтением и записью другая транзакция может выполнить собственное обновление, если выбранный режим изоляции не удерживает необходимую блокировку.
В примере число 70 — результат, вычисленный на основании ранее прочитанного значения. Если две транзакции используют разные вычисленные значения, последняя запись может затереть первую. Конкретные детали блокировок и поведения при конфликте зависят от СУБД, поэтому нельзя считать, что одно название уровня изоляции полностью описывает защиту от потери обновления.
Надёжные способы защиты:
Выбор зависит от бизнес-правила. Для короткой операции над одной строкой часто подходит атомарное обновление; для сложного расчёта по нескольким данным требуется блокировка, версия или более строгая изоляция. Повышение уровня изоляции не всегда автоматически устраняет именно потерю обновления: необходимо проверить семантику конкретной СУБД и форму операции.
В сервисе продажи билетов два запроса одновременно видят один последний билет. Вариант с обычным чтением, расчётом в приложении и последующей записью допускает потерю изменения или двойную продажу. Вариант с блокировкой строки предотвращает конфликт, но при всплеске нагрузки создаёт очередь ожидающих запросов.
Для короткой операции выбран атомарный переход остатка: уменьшить количество только при условии, что оно больше нуля. Такой подход не хранит в приложении устаревшее значение, выполняет проверку вместе с изменением и позволяет определить успешную покупку по числу изменённых строк.
Если операция включает резервирование, оплату и обращение к внешней системе, одной такой записи недостаточно. Тогда применяют транзакцию с подходящей блокировкой или версией, фиксируют состояние заказа, а внешние действия согласуют отдельными механизмами, например повторяемыми обработчиками и идемпотентностью.
1. Всегда ли READ COMMITTED допускает потерю обновления?
Нет универсального ответа для всех СУБД. При READ COMMITTED часто допускается сценарий «прочитал — вычислил — записал», но конкретная СУБД, драйвер, способ обновления и наличие блокировки могут изменить поведение. Поэтому нужно анализировать фактические операторы, момент взятия блокировок и реакцию на конфликт, а не делать вывод только по названию уровня изоляции.
2. Чем потеря обновления отличается от некорректного чтения?
При некорректном чтении транзакция видит данные, которые другая транзакция ещё не зафиксировала; это называют грязным чтением. При потере обновления обе транзакции могут читать только зафиксированные данные, но последующая запись одной из них затирает результат другой. Это разные аномалии, требующие разных средств защиты.
3. Почему увеличение уровня изоляции не всегда лучший способ исправления?
Более строгая изоляция может увеличить число блокировок, ожиданий, откатов и повторов транзакций. Она также может быть избыточной, если проблему решает одна атомарная операция или проверка версии. Сначала определяют инвариант — например, остаток не должен стать отрицательным и две покупки не должны использовать один билет, — затем выбирают минимальный механизм, который гарантирует именно этот инвариант.