главная/Race Condition
Race condition

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;