Представьте API профиля: два клиента одновременно меняют один ресурс без передачи версии. Какой дефект може...

Представьте API профиля: два клиента одновременно меняют один ресурс без передачи версии. Какой дефект может проявиться?

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

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

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

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

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

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

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

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

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

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

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

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

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

Для HTTP-API эту модель можно выразить через ETag и условие If-Match. ETag является представлением версии ресурса, но сам по себе ничего не защищает: клиент обязан передать его в условии, а сервер обязан проверить условие перед изменением.

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

Серверная блокировка — альтернативный подход. Она может быть уместна для коротких операций внутри одного хранилища, но распределённая блокировка требует управления временем жизни, отказами и потерей владельца, поэтому не должна подменять проверку версии без необходимости.

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

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

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

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

  1. Достаточно ли вернуть клиенту ETag, чтобы защитить ресурс?

Нет. ETag только идентифицирует конкретное представление ресурса. Защита появляется, когда клиент передаёт это значение в условии обновления, а сервер атомарно сравнивает его с текущим значением и отклоняет устаревшую запись.

  1. Почему нельзя просто принять последнюю запись и считать её источником истины?

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

  1. Когда частичные обновления позволяют избежать конфликта?

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