Ответ
Оптимистичная блокировка — это стратегия управления параллельным доступом, которая позволяет избежать проблемы "потерянного обновления" (lost update) без использования реальных блокировок ресурсов на сервере. Основная идея в том, чтобы перед обновлением ресурса проверить, не был ли он изменен кем-то другим с момента его последнего чтения.
В HTTP это реализуется с помощью условных запросов (Conditional Requests), которые используют специальные заголовки.
Основные механизмы:
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
).
Last-Modified и
If-Unmodified-Since
/If-Modified-Since
Last-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 {
// Обработать конфликт: сообщить пользователю, что данные устарели
}