Laravel зчитує сесію на початку запиту, а записує цілком у кінці. Якщо два запити одного користувача виконуються паралельно, кожен працює зі своєю копією, і той, що завершиться останнім, перезапише зміни іншого.
Приклад:
Запит A (додати товар у кошик) читає кошик [1] записує [1, 2]
Запит B (додати інший товар) читає кошик [1] записує [1, 3] <- товар 2 втрачено
Так буває з кількома AJAX-запитами одночасно, подвійним кліком, кількома вкладками. На відміну від стандартних файлових сесій PHP, драйвери Laravel (database, redis, file) не блокують сесію за замовчуванням.
Блокування сесії для конкретних маршрутів:
Route::post('/cart/items', AddToCartController::class)
->block($lockSeconds = 10, $waitSeconds = 10);
- запит бере атомарне блокування для сесії (через кеш) і тримає його до
$lockSeconds; - наступний запит цієї ж сесії до маршруту з
blockчекає до$waitSeconds, а якщо не дочекався - отримуєLockTimeoutException; - запити виконуються послідовно, кожен бачить зміни попереднього.
Вимоги: драйвер кешу з атомарними блокуваннями (redis, memcached, database, dynamodb) і драйвер сесій не cookie.
Ціна: паралельність для одного користувача зникає - повільний запит затримує інші. Тому блокують лише маршрути, які змінюють сесію, а не весь застосунок.
Часто краще не тримати такі дані в сесії взагалі:
- кошик, чернетки, налаштування - у базі з транзакціями чи атомарними операціями. Там конкурентність розв'язується надійніше й не залежить від сесії;
- для лічильників - атомарний інкремент у Redis чи базі.
Як виявити проблему: «зникають» щойно додані дані, флеш-повідомлення показується не там, кошик втрачає товари при швидких кліках. Відтворюється одночасною відправкою двох запитів (наприклад, Promise.all з двох fetch).