Представьте API профиля: два клиента одновременно меняют один ресурс без передачи версии. Какой дефект может проявиться?
Возникает потеря обновления: более поздняя запись может затереть изменения, сделанные другим клиентом. Для защиты сервер должен проверять версию ресурса или условие актуальности перед записью и отклонять устаревшее изменение.
Проблема появилась из-за перехода от локальных транзакций и блокировок к распределённым API, где клиент читает ресурс, некоторое время изменяет его и затем отправляет запись обратно. За это время ресурс могут изменить другие клиенты.
Постоянные блокировки на уровне приложения плохо подходят для долгих сетевых операций: они удерживают ресурсы, усложняют отказоустойчивость и плохо масштабируются. Поэтому для веб-API часто применяют оптимистический контроль конкурентности: конфликт предполагается редким, а версия проверяется непосредственно перед записью.
Пусть два клиента прочитали профиль с версией 10. Первый изменил телефон и сохранил профиль, получив версию 11. Второй, не зная об этом изменении, отправил старое полное состояние с новым адресом.
Если сервер безусловно принимает вторую запись, телефон первого клиента исчезает. Это особенно опасно для финансовых, административных и других данных, где молчаливое затирание не может считаться корректным разрешением конфликта.
Сервер связывает ресурс с версией, счётчиком изменения или другой проверяемой меткой. Клиент получает эту метку вместе с ресурсом и передаёт её при обновлении; сервер принимает запись только если текущая версия всё ещё совпадает с переданной.
Условие должно проверяться атомарно вместе с записью. Проверка, выполненная отдельным чтением, недостаточна: между чтением версии и сохранением другой запрос может успеть изменить ресурс.
При несовпадении версий сервер не должен молча перезаписывать данные. Он возвращает конфликт, а клиент перечитывает актуальное состояние, повторно применяет пользовательское изменение и предлагает разрешить конфликт либо автоматически объединяет изменения, если такая политика безопасна.
Для HTTP-API эту модель можно выразить через ETag и условие If-Match. ETag является представлением версии ресурса, но сам по себе ничего не защищает: клиент обязан передать его в условии, а сервер обязан проверить условие перед изменением.
Частичное обновление не устраняет проблему автоматически. Если две операции меняют разные поля, сервер может безопасно объединить их при явно определённых правилах; если они меняют одно поле или взаимосвязанные поля, требуется конфликт или доменная логика слияния.
Серверная блокировка — альтернативный подход. Она может быть уместна для коротких операций внутри одного хранилища, но распределённая блокировка требует управления временем жизни, отказами и потерей владельца, поэтому не должна подменять проверку версии без необходимости.
В административной панели два оператора одновременно редактируют карточку клиента. Вариант с безусловной записью прост, но приводит к тихой потере одного из наборов изменений. Вариант с распределённой блокировкой предотвращает одновременное редактирование, однако усложняет работу при закрытии браузера, сетевых сбоях и зависших сессиях.
Был выбран оптимистический контроль конкурентности: карточка содержит версию, а сохранение допускается только для последней прочитанной версии. При конфликте второй оператор получает актуальную карточку и явно решает, какие изменения сохранить. В результате система перестаёт скрывать конфликт и сохраняет оба исходных варианта для принятия решения.
Нет. ETag только идентифицирует конкретное представление ресурса. Защита появляется, когда клиент передаёт это значение в условии обновления, а сервер атомарно сравнивает его с текущим значением и отклоняет устаревшую запись.
Такой подход реализует стратегию последняя запись побеждает, но не отличает осознанное решение пользователя от устаревшего состояния. Он допустим только при явно принятой семантике, когда потеря предыдущего значения безопасна, например для некоторых некритичных настроек.
Если операции затрагивают независимые поля, сервер может применить их отдельно или объединить по определённым правилам. Однако независимость должна быть доменной, а не только структурной: изменение лимита и изменение статуса могут находиться в разных полях, но оставаться логически связанными. Для пересекающихся или взаимозависимых изменений всё равно нужна проверка версии и явное разрешение конфликта.