Limit miejsc, którego nie da się przekroczyć
Dwóch rodziców klika „Zapisz” w tej samej sekundzie, a zostało jedno miejsce. Jak zablokować overbooking w Laravelu transakcją i blokadą wiersza.
Najczęstszy błąd w systemach zapisów: sprawdzenie liczby wolnych miejsc i zapis rezerwacji to dwa osobne zapytania. Między nimi może wejść drugi request. Przy otwarciu zapisów na popularny turnus to nie teoria, tylko pewność.
Rozwiązanie: transakcja i lockForUpdate
<?php
use App\Exceptions\CampSessionFullException;
use App\Models\Booking;
use App\Models\CampSession;
use Illuminate\Support\Facades\DB;
function reserve(int $sessionId, array $data): Booking
{
return DB::transaction(function () use ($sessionId, $data) {
// Blokujemy wiersz turnusu do końca transakcji.
// Drugi request czeka tutaj, zamiast liczyć te same miejsca.
$session = CampSession::whereKey($sessionId)
->lockForUpdate()
->firstOrFail();
$taken = $session->bookings()
->whereIn('status', ['pending', 'paid'])
->count();
if ($taken >= $session->capacity) {
throw new CampSessionFullException($session);
}
return $session->bookings()->create($data + ['status' => 'pending']);
}, attempts: 3); // ponów przy deadlocku
}
Dlaczego to działa
SELECT … FOR UPDATE w InnoDB zakłada blokadę na wierszu turnusu. Drugi request, który chce zablokować ten sam wiersz, czeka, aż pierwsza transakcja się zakończy, i dopiero wtedy liczy miejsca, już z nową rezerwacją.
Co jeszcze
- Rezerwacje
pendingteż zajmują miejsce. Nieopłacone anuluje osobny job po upływie terminu płatności, który zwalnia miejsce. - Komunikat dla rodzica nie może brzmieć „Błąd 500”. Wyjątek
CampSessionFullExceptionmapujemy na czytelną informację i propozycję listy rezerwowej. - Limit sprawdzany w JavaScripcie to tylko podpowiedź dla użytkownika. Prawdę mówi serwer.