Ответ
Оптимистичная блокировка — это стратегия управления параллельным доступом, которая позволяет избежать проблемы "потерянного обновления" (lost update) без использования реальных блокировок ресурсов на сервере. Основная идея в том, чтобы перед обновлением ресурса проверить, не был ли он изменен кем-то другим с момента его последнего чтения.
В HTTP это реализуется с помощью условных запросов (Conditional Requests), которые используют специальные заголовки.
Основные механизмы:
ETag и
If-Match/If-None-MatchETag(Entity Tag) — это заголовок ответа, содержащий уникальный идентификатор версии ресурса (например, хэш контента).If-Match— это заголовок запроса. Клиент, желающий обновить ресурс, отправляетPUTилиPATCHзапрос с этим заголовком, указываяETag, который он получил при последнем чтении. Сервер выполнит обновление только если текущийETagресурса совпадает с присланным. В противном случае сервер вернет ошибку412 Precondition Failed.If-None-Matchчаще используется для кэширования (GET-запросы), чтобы избежать повторной загрузки ресурса, если он не изменился (сервер вернет304 Not Modified).
Last-Modified и
If-Unmodified-Since/If-Modified-SinceLast-Modified— заголовок ответа, содержащий дату и время последнего изменения ресурса.If-Unmodified-Since— заголовок запроса, аналогичныйIf-Match, но использующий временную метку. Запрос будет выполнен, только если ресурс не изменялся с указанной даты.
Пример рабочего процесса с ETag:
- Клиент делает
GET /api/items/1и получает в ответеETag: "v1-abcde12345". - Клиент изменяет данные и отправляет
PUT /api/items/1с заголовкомIf-Match: "v1-abcde12345". - Сервер:
- Если текущий
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 {
// Обработать конфликт: сообщить пользователю, что данные устарели
}