Какие механизмы в 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 {
    // Обработать конфликт: сообщить пользователю, что данные устарели
}

Ответ 18+ 🔞

А, слушай, вот это тема! Оптимистичная блокировка, блядь! Это ж как в жизни — ты думаешь, что всё под контролем, а потом раз — и кто-то уже твою булку с маслом сожрал. Но без драк, на цивильном уровне, ёпта.

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

Вот как эта подстраховка в HTTP работает, на примере ETag. Это как татуировка на ресурсе, версионка. Сделал GET запрос — сервер тебе в ответе, кроме данных, подсунул ETag: "v1-abcde12345". Типа, «запомни эту метку, браток».

Теперь ты, такой весь из себя обновлённый, собираешься отправить PUT или PATCH. Но не просто так, а с читерским заголовком If-Match: "v1-abcde12345". Ты как бы говоришь серверу: «Эй, я меняю ту версию, которую видел. Если она до сих пор такая же — пропусти. Если уже кто-то успел её подшаманить — дай мне пизды».

Сервер смотрит. Ага, ETag у ресурса всё ещё "v1-abcde12345". Ну всё, красава, обновляй. Возвращает 200 OK. Но если за это время какая-то мартышлюшка уже всё поменяла и ETag стал "v2-fghij67890" — сервер тебе в ответ: 412 Precondition Failed. Всё, приехали. Конфликт, блядь. Тебе теперь надо пользователю сказать: «Слушай, там пока ты кнопку жал, данные уже поменялись. Обнови страницу, посмотри, что там новенького, и решай, что делать».

Есть ещё вариант с датами — Last-Modified и If-Unmodified-Since. Принцип тот же, только вместо хэша — штамп времени. Но ETag надёжнее, потому что время на серверах может плавать, а хэш — он точный.

Вот, смотри, как это в коде на 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 {
    // Обработать конфликт: сообщить пользователю, что данные устарели
}

Видишь? Всё просто. Отправил запрос, получил 412 — и волнение ебать, терпения ноль. Надо конфликт решать. Но зато никаких реальных блокировок на сервере, всё летает. Красота, да? Прям как в той истории про Герасима — думал, что он один с Муму, а там, оказывается, барыня уже приказ отдала. Вот и приходится метаться. Только здесь, слава богу, собачку топить не надо, а просто данные перечитать.