Охх... Никогда не думал, что столкнусь с race condition в PHP )))
В других языках такое решают общим хранилищем данных для всех параллельных потоков и использованием мьютекса.
Мы же можем воспользоваться инструментами Redis, которые сделали специально для решения таких вещей. У Redis есть специальные флаги, позволяющие использовать атомарные операции. Команда SET с параметрами NX и EX.
- SET key value NX — установить ключ, только если он Not eXists (не существует).
- SET key value EX seconds — установить ключ с временем жизни (eXpire) в секундах.
Комбинация этих двух флагов позволяет создать атомарную операцию "захватить блокировку на N секунд".
handler.php
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 1. Создаем уникальный ключ для этого конкретного запроса
$lockKey = 'request_lock:' . md5(json_encode($_REQUEST));
// 2. Пытаемся захватить ключ на 10 секунд
// Эта команда атомарная: только один процесс из двух победит.
$isLockAcquired = $redis->set($lockKey, '1', ['nx', 'ex' => 10]);
if ($isLockAcquired) {
// КЛЮЧ НАШ! Делаем свою работу
try {
// ... обращаемся к стороннему сервису, запускаем worker.php или ваще что угодно ...
// отвечаем клиенту об успехе операции
http_response_code(200);
echo json_encode(['status' => 'success']);
} catch(\Throwable $e) {
// Освобождаем ключ для будущих запросов в случае ошибки, чтобы не ждать 10с для переотправки.
$redis->del($lockKey);
// отвечаем клиенту, что произошла ошибка
http_response_code(500);
echo json_encode(['status' => 'error']);
}
} else {
// КЛЮЧ УЖЕ КЕМ-ТО ЗАНЯТ. Ничего не делаем.
// Просто отвечаем клиенту, что все ок или что запрос дублируется.
http_response_code(429); // Too Many Requests
echo json_encode(['status' => 'error', 'message' => 'Request already in progress']);
exit;
}