
Race Condition
Race condition — это ситуация, когда два или больше процессов (или пользователей) одновременно обращаются к одним и тем же данным.
Каждый думает, что данные “свободны”, и в итоге система делает неожиданное обновление или теряет часть данных.
Допустим, у нас есть интернет-магазин с одним билетом:
// Оба пользователя вызывают этот код почти одновременно
$ticket = DB::table('tickets')->where('id', 1)->first();
if ($ticket->available) {
DB::table('tickets')->where('id', 1)->update(['available' => false]);
echo "Билет куплен!";
} else {
echo "Уже продан.";
}
Если два запроса придут в один момент, оба прочитают
$ticket->available = true
Оба подумают, что билет ещё свободен, и оба успеют его “купить”.
Теперь у нас один билет, но две продажи.
Код выполняется быстро, но база данных обрабатывает каждый запрос отдельно.
Когда два запроса приходят почти одновременно, между чтением и обновлением возникает щель во времени — именно в неё и “попадает” вторая операция.
Как исправить
Транзакция и блокировка строки
Используй блокировку через lockForUpdate(), чтобы второй процесс ждал, пока первый закончит:
DB::transaction(function () {
$ticket = DB::table('tickets')
->where('id', 1)
->lockForUpdate() // блокирует строку до конца транзакции
->first();
if ($ticket->available) {
DB::table('tickets')->where('id', 1)->update(['available' => false]);
echo "Билет куплен!";
} else {
echo "Уже продан.";
}
});
Так второй запрос не сможет прочитать строку, пока первый её не обновит.
Атомарное обновление
Можно сделать обновление в один шаг:
$updated = DB::table('tickets')
->where('id', 1)
->where('available', true)
->update(['available' => false]);
if ($updated) {
echo "Билет куплен!";
} else {
echo "Уже продан.";
}
Если другой процесс уже успел купить билет, update просто вернёт 0 — значит, ничего не изменилось.
Как это выглядит на SQL уровне:
START TRANSACTION;
SELECT * FROM tickets WHERE id = 1 FOR UPDATE;
UPDATE tickets SET available = false WHERE id = 1;
COMMIT;