Проблема втраченого оновлення. Два менеджери відкрили одне замовлення. Перший змінив адресу доставки й зберіг. Другий, дивлячись на стару версію, змінив коментар і зберіг - і перезаписав адресу першого старим значенням. Ніхто не отримав помилки.
Оптимістичне блокування через умовні запити:
1. Сервер віддає версію ресурсу в ETag:
GET /api/orders/42
HTTP/1.1 200 OK
ETag: "v17"
2. Клієнт оновлює з умовою «лише якщо версія досі та сама»:
PATCH /api/orders/42
If-Match: "v17"
{"comment": "Подзвонити перед доставкою"}
3. Сервер перевіряє:
- версія збігається - оновлює, повертає новий
ETag: "v18"; - версія змінилася -
412 Precondition Failed, нічого не змінює. Клієнт перечитує ресурс, показує користувачу зміни й пропонує злити їх.
Якщо клієнт не надіслав If-Match для ресурсу, де конфлікти критичні, сервер може вимагати його: 428 Precondition Required (RFC 6585).
Реалізація в Laravel:
public function update(UpdateOrderRequest $request, Order $order): JsonResponse
{
$expected = trim($request->header('If-Match', ''), '"');
$updated = Order::whereKey($order->id)
->where('version', (int) ltrim($expected, 'v'))
->update([...$request->validated(), 'version' => DB::raw('version + 1')]);
abort_if($updated === 0, 412, 'Ресурс змінено іншим користувачем');
return OrderResource::make($order->fresh())
->response()
->header('ETag', '"v'.$order->fresh()->version.'"');
}
Ключове - перевірка й оновлення одним атомарним UPDATE ... WHERE version = ?. Варіант «прочитати версію, порівняти в PHP, потім записати» має ту саму гонитву, від якої ми захищаємося.
Джерело версії: окрема колонка version (лічильник), updated_at з мікросекундами або хеш вмісту. Лічильник найнадійніший: updated_at може збігтися при двох змінах в одну секунду.
Чим це краще за песимістичне блокування (заблокувати запис на час редагування): не потрібно тримати блокування між HTTP-запитами, немає «завислих» блокувань, коли користувач закрив вкладку. Конфлікти рідкісні - і обробляються лише тоді, коли справді стаються.
Слабкі ETag (W/"v17") для If-Match не підходять - порівняння має бути сильним.