Представьте, что две транзакции пытаются изменить одну строку до фиксации первой. Как СУБД предотвращает «г...

Представьте, что две транзакции пытаются изменить одну строку до фиксации первой. Как СУБД предотвращает «грязную запись»?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Основной компромисс — между ожиданием и отказом. Ожидание повышает вероятность успешного завершения, но увеличивает задержку и может способствовать взаимным блокировкам; немедленное прерывание уменьшает задержку блокировки, но требует корректной обработки ошибок и повторного выполнения всей транзакции.

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

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

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

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

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

  1. Всегда ли вторая транзакция обязана ждать при конфликте записи?

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

  1. Достаточно ли запрета грязных записей, чтобы исключить потерю обновления?

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

  1. Почему после конфликта следует повторять всю транзакцию, а не только UPDATE?

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