Какие механизмы в HTTP используются для реализации оптимистичных блокировок (optimistic locking)?

Ответ

Оптимистичная блокировка — это стратегия управления параллельным доступом, которая позволяет избежать проблемы "потерянного обновления" (lost update) без использования реальных блокировок ресурсов на сервере. Основная идея в том, чтобы перед обновлением ресурса проверить, не был ли он изменен кем-то другим с момента его последнего чтения.

В HTTP это реализуется с помощью условных запросов (Conditional Requests), которые используют специальные заголовки.

Основные механизмы:

  1. ETag и If-Match / If-None-Match

    • ETag (Entity Tag) — это заголовок ответа, содержащий уникальный идентификатор версии ресурса (например, хэш контента).
    • If-Match — это заголовок запроса. Клиент, желающий обновить ресурс, отправляет PUT или PATCH запрос с этим заголовком, указывая ETag, который он получил при последнем чтении. Сервер выполнит обновление только если текущий ETag ресурса совпадает с присланным. В противном случае сервер вернет ошибку 412 Precondition Failed.
    • If-None-Match чаще используется для кэширования (GET-запросы), чтобы избежать повторной загрузки ресурса, если он не изменился (сервер вернет 304 Not Modified).
  2. Last-Modified и If-Unmodified-Since / If-Modified-Since

    • Last-Modified — заголовок ответа, содержащий дату и время последнего изменения ресурса.
    • If-Unmodified-Since — заголовок запроса, аналогичный If-Match, но использующий временную метку. Запрос будет выполнен, только если ресурс не изменялся с указанной даты.

Пример рабочего процесса с ETag:

  1. Клиент делает GET /api/items/1 и получает в ответе ETag: "v1-abcde12345".
  2. Клиент изменяет данные и отправляет PUT /api/items/1 с заголовком If-Match: "v1-abcde12345".
  3. Сервер:
    • Если текущий ETag ресурса все еще "v1-abcde12345", сервер обновляет ресурс и возвращает 200 OK.
    • Если другой клиент уже обновил ресурс и его ETag теперь "v2-fghij67890", сервер отклоняет запрос и возвращает 412 Precondition Failed.

Пример в Go:

// Клиентский код
req, _ := http.NewRequest("PUT", url, requestBody)
// Устанавливаем заголовок для оптимистичной блокировки
req.Header.Set("If-Match", "полученный-ранее-etag")

client := &http.Client{}
resp, err := client.Do(req)
if resp.StatusCode == http.StatusPreconditionFailed {
    // Обработать конфликт: сообщить пользователю, что данные устарели
}