АрхитектураМикросервисы и интеграцииАрхитектор распределённых систем

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

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

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

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

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

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

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

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

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

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

Клиент читает ресурс с версией 7, меняет его локально и отправляет обновление. До записи другой клиент уже изменил ресурс, повысив версию до 8.

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

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

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

В контракт включают версию, номер ревизии, временной маркер или эквивалентный валидатор состояния. Клиент получает ресурс вместе с этим маркером, а при обновлении сообщает: «примени изменение только если текущая версия равна прочитанной».

Сервис выполняет проверку и изменение как одну атомарную операцию:

  1. сравнивает ожидаемую версию с текущей;
  2. при совпадении применяет изменение и увеличивает версию;
  3. при несовпадении не изменяет ресурс и возвращает явный конфликт.

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

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

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

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

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

Две внутренние системы редактируют карточку товара. Одна меняет цену, другая — описание. Обе прочитали карточку в версии 42, затем первая сохранила цену как версию 43, а вторая попыталась записать полную устаревшую копию.

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

Выбрали проверку версии на стороне сервиса-владельца. Второе обновление получает конфликт, перечитывает карточку и формирует новую операцию на версии 43; для полей с независимой семантикой система может объединить изменения, а для цены требует явного решения пользователя.

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

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

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

Нет. Последовательность «прочитать версию — затем выполнить обновление» содержит окно гонки: другой клиент может изменить ресурс после проверки. Сравнение версии и запись должны быть одной атомарной операцией на стороне владельца данных. Иначе механизм лишь создаёт видимость защиты.

  1. Что должен делать клиент после получения конфликта версии?

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

  1. Почему версия полезнее времени последнего изменения?

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