При формировании ответа профиль собирается двумя чтениями. Какой механизм нужен, чтобы параллельное обновление не создало внутренне противоречивый снимок данных?
SELECT balance INTO :balance
FROM accounts
WHERE user_id = :id;
-- В этот момент другой запрос меняет account_limits
SELECT daily_limit INTO :limit
FROM account_limits
WHERE user_id = :id;
Нужна транзакция с согласованным снимком чтения, например с уровнем изоляции REPEATABLE READ или эквивалентным механизмом snapshot read. Тогда оба запроса увидят данные в одном логическом состоянии, а не результаты, полученные в разные моменты времени.
Раздельные чтения без общей транзакции стали проблемой по мере роста параллелизма: разные операции могут одновременно изменять связанные записи. Обычного выполнения отдельных запросов недостаточно, потому что каждый запрос может получить собственную версию данных.
Транзакции и уровни изоляции появились как способ формально управлять видимостью конкурентных изменений. Для чтения нескольких связанных объектов важна не только целостность каждой строки, но и согласованность всего набора данных.
В примере сначала читается баланс, затем лимит. Между этими запросами другой процесс может изменить лимит или баланс. В результате клиент получит комбинацию значений, которая никогда не существовала одновременно.
Такой дефект особенно опасен для финансовых расчётов, проверки доступного лимита и формирования агрегированных ответов. Ошибка может быть редкой и не воспроизводиться в тестах, но при высокой конкуренции приводить к неверным решениям или противоречивым данным в интерфейсе.
Оба чтения нужно выполнить внутри одной транзакции с требуемой гарантией согласованности:
При snapshot-изоляции транзакция выбирает логический снимок данных, и последующие чтения в её рамках используют тот же снимок. Обновления, завершившиеся после момента снимка, не попадут только во второй запрос.
Уровень READ COMMITTED обычно гарантирует согласованность каждого отдельного запроса, но не обязательно всей последовательности: два SELECT могут увидеть разные состояния. REPEATABLE READ лучше подходит для согласованного чтения, однако конкретная реализация и возможные конфликты зависят от выбранной СУБД.
Если операция не только читает, но и принимает решение перед записью, одной согласованности снимка может быть недостаточно. Нужно дополнительно учитывать конкурирующие изменения: применять блокировки, проверять версию записи или обрабатывать конфликт сериализации. Чем сильнее изоляция, тем выше потенциальная цена в виде блокировок, откатов и снижения пропускной способности.
Согласованный снимок также не исправляет рассинхронизацию между разными хранилищами. Если баланс находится в одной базе, а лимит — в другой, локальная транзакция не обеспечит общий снимок без специального протокола, например распределённой транзакции или модели, допускающей событийную согласованность.
Сервис возвращал клиенту доступный кредитный лимит, читая текущую задолженность и лимит из двух таблиц. В редких случаях ответ показывал старую задолженность вместе с новым лимитом, потому что между запросами завершалось обновление одной из записей.
Рассматривались три варианта. Повторный запрос уменьшал вероятность ошибки, но не давал гарантии. Объединение данных в одну таблицу упрощало чтение, но усложняло модель и миграцию. Транзакция с согласованным снимком сохранила текущую схему и обеспечила единое состояние для чтения.
Выбрали третий вариант, а для операций, изменяющих остаток, добавили проверку версии и обработку конфликтов. Это разделило две задачи: snapshot-изоляция обеспечила согласованный ответ, а контроль версии защитил бизнес-решение от конкурентной записи.
Нет, не всегда. Этот уровень обычно делает каждый запрос согласованным относительно собственного момента выполнения, но между запросами может быть зафиксирована другая транзакция. Поэтому два результата могут относиться к разным состояниям данных.
Нет. Он обеспечивает согласованный снимок чтения, но конкурентная запись может привести к конфликту, блокировке или необходимости повторить транзакцию — в зависимости от СУБД и операции. Код должен уметь корректно обрабатывать такие исходы, а не считать транзакцию безусловно успешной.
Один запрос обычно получает единое согласованное представление в рамках своей операции и часто устраняет окно между двумя чтениями. Но это не универсальная замена транзакции: если после чтения выполняются дополнительные проверки или записи, для всей последовательности всё равно могут потребоваться транзакция и защита от конкурентных изменений.