← Назад к вопросам

Что такое гонка запросов?

1.8 Middle🔥 151 комментариев
#Архитектура и паттерны#Безопасность

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое гонка запросов (Race Condition)?

Гонка запросов (или состояние гонки, race condition) — это ошибка проектирования параллельных систем, при которой конечный результат выполнения операций зависит от порядка или времени их исполнения в условиях одновременного доступа к общим ресурсам (например, данным в базе, файлам, переменным в памяти). Это классическая проблема для backend-разработки, особенно в PHP-приложениях, работающих в многопользовательской среде (веб-серверы, очереди задач).

Почему возникает гонка запросов в PHP?

Хотя PHP традиционно использует неблокирующую модель обработки HTTP-запросов (каждый запрос выполняется в отдельном процессе или потоке), гонки возникают, когда:

  1. Несколько запросов одновременно читают общий ресурс (например, баланс пользователя в БД).
  2. Каждый запрос на основе прочитанных данных вычисляет новое состояние (списание средств, увеличение счётчика).
  3. Запись нового состояния обратно в ресурс выполняется без синхронизации.

Пример в e-commerce: два одновременных запроса на списание средств с одного счёта могут прочитать одинаковый остаток, что приведёт к двойному списанию.

Пример кода с гонкой

Рассмотрим типичный сценарий — обновление счётчика просмотров статьи без синхронизации:

// НЕВЕРНО: Уязвимый код с гонкой
$articleId = (int)$_GET['article_id'];
$pdo = new PDO('mysql:host=localhost;dbname=blog', 'user', 'pass');

// 1. Чтение текущего значения
$stmt = $pdo->query("SELECT views FROM articles WHERE id = $articleId");
$currentViews = $stmt->fetchColumn();

// 2. Вычисление нового значения (время между чтением и записью — окно уязвимости)
$newViews = $currentViews + 1;

// 3. Запись нового значения
$pdo->exec("UPDATE articles SET views = $newViews WHERE id = $articleId");

Проблема: Если два запроса выполнят шаг 1 одновременно, оба прочитают одно и то же значение $currentViews, и оба установят views = $currentViews + 1. Вместо увеличения на 2 счётчик увеличится только на 1.

Методы предотвращения гонок в PHP

1. Атомарные операции в БД

Использование SQL-операций, которые выполняют чтение и запись в одной инструкции:

UPDATE articles SET views = views + 1 WHERE id = :articleId
$stmt = $pdo->prepare("UPDATE articles SET views = views + 1 WHERE id = ?");
$stmt->execute([$articleId]);

2. Транзакции с подходящим уровнем изоляции

В MySQL/InnoDB используйте SELECT ... FOR UPDATE для пессимистичной блокировки:

$pdo->beginTransaction();
try {
    $stmt = $pdo->prepare("SELECT views FROM articles WHERE id = ? FOR UPDATE");
    $stmt->execute([$articleId]);
    $currentViews = $stmt->fetchColumn();
    
    $newViews = $currentViews + 1;
    
    $pdo->prepare("UPDATE articles SET views = ? WHERE id = ?")
        ->execute([$newViews, $articleId]);
    
    $pdo->commit();
} catch (Exception $e) {
    $pdo->rollBack();
    throw $e;
}

3. Мьютексы (блокировки) на уровне приложения

Использование систем распределённых блокировок (Redis, Memcached) для кластерных сред:

$redis = new Redis();
$redis->connect('localhost');

$lockKey = "article_lock:$articleId";
// Пытаемся получить блокировку с таймаутом
if ($redis->set($lockKey, 1, ['nx', 'ex' => 5])) {
    try {
        // Критическая секция
        $currentViews = $pdo->query("SELECT views FROM articles WHERE id = $articleId")->fetchColumn();
        $pdo->exec("UPDATE articles SET views = " . ($currentViews + 1) . " WHERE id = $articleId");
    } finally {
        $redis->del($lockKey);
    }
} else {
    // Обработка ситуации, когда блокировка не получена
    throw new Exception('Слишком много одновременных запросов');
}

4. Оптимистичная блокировка (версионирование)

Добавление поля-версии для проверки изменений:

ALTER TABLE articles ADD COLUMN version INT DEFAULT 0;
$stmt = $pdo->prepare("SELECT views, version FROM articles WHERE id = ?");
$stmt->execute([$articleId]);
[$currentViews, $currentVersion] = $stmt->fetch();

$newViews = $currentViews + 1;

// Обновляем только если версия не изменилась
$stmt = $pdo->prepare(
    "UPDATE articles SET views = ?, version = version + 1 
     WHERE id = ? AND version = ?"
);
$stmt->execute([$newViews, $articleId, $currentVersion]);

if ($stmt->rowCount() === 0) {
    // Кто-то уже обновил запись — повторить операцию
    throw new ConcurrentModificationException();
}

Особенности в контексте PHP

  1. Отсутствие общей памяти между процессами — гонки обычно возникают на уровне БД, файловой системы или внешних сервисов.
  2. Очереди задач (RabbitMQ, Kafka) — требуют идемпотентности обработчиков, так как возможно повторное выполнение задачи.
  3. Кеширование — инвалидация кеша при конкурентных записях требует стратегий типа "cache aside with lock" или "write-through".

Практические рекомендации

  • Атомарность — всегда предпочитайте атомарные операции БД, когда это возможно.
  • Минимизация окон гонок — сокращайте время между чтением и записью в критических секциях.
  • Тестирование — используйте нагрузочное тестирование (Apache JMeter, k6) для выявления гонок.
  • Проектирование — применяйте архитектурные паттерны (CQRS, Event Sourcing), которые естественным образом снижают риски гонок через иммутабельность данных.

Гонка запросов — не просто теоретическая проблема, а реальная уязвимость, приводящая к финансовым потерям, повреждению данных и неконсистентности состояния системы. В PHP-разработке её предотвращение требует осознанного выбора механизмов синхронизации на уровне БД или приложения.

Что такое гонка запросов? | PrepBro