Очереди задач в PHP: руководство по реализации и оптимизации

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Работа с очередями задач в PHP: от простого к сложному

В современных веб-приложениях часто возникает необходимость выполнять ресурсоёмкие или длительные операции, не заставляя пользователя ждать. Отправка email-уведомлений, генерация отчётов, обработка изображений, интеграция с внешними API — все эти задачи идеально подходят для асинхронного выполнения. Именно здесь на помощь приходят очереди задач (job queues). В этой статье мы подробно рассмотрим, как реализовать и оптимизировать работу с очередями в PHP, начиная с простейших решений и заканчивая промышленными инструментами.

Что такое очередь задач и зачем она нужна?

Очередь задач (job queue) — это механизм, который позволяет отложить выполнение некоторой работы на потом. Вместо того чтобы выполнять задачу немедленно в рамках HTTP-запроса, вы помещаете её в очередь. Специальный воркер (worker) или группа воркеров извлекают задачи из очереди и обрабатывают их в фоновом режиме.

Основные преимущества использования очередей:

  • Ускорение ответа пользователю: Пользователь получает ответ сразу, а тяжёлая работа выполняется асинхронно.
  • Масштабирование: Вы можете запустить несколько воркеров для параллельной обработки задач.
  • Надёжность: Если задача не выполнилась (например, из-за временной ошибки), её можно повторить позже.
  • Разгрузка основного сервера: Фоновые процессы не блокируют обработку новых HTTP-запросов.

Простая очередь на основе базы данных

Самый простой способ организовать очередь в PHP — использовать таблицу в базе данных (например, MySQL). Этот подход не требует установки дополнительного программного обеспечения и отлично подходит для небольших проектов.

Шаг 1: Создание таблицы jobs

CREATE TABLE jobs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    queue_name VARCHAR(255) DEFAULT 'default',
    payload TEXT NOT NULL,
    status ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    available_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    attempts INT DEFAULT 0
);

Шаг 2: Добавление задачи в очередь

function addJob(string $queueName, array $data): void {
    $pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'password');
    $stmt = $pdo->prepare(
        "INSERT INTO jobs (queue_name, payload, available_at) 
         VALUES (?, ?, NOW())"
    );
    $stmt->execute([$queueName, json_encode($data)]);
}

// Пример использования
addJob('emails', ['to' => 'user@example.com', 'subject' => 'Привет!']);

Шаг 3: Создание воркера (worker.php)

// Бесконечный цикл для обработки задач
while (true) {
    $pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'password');
    
    // Захват задачи с блокировкой строки
    $stmt = $pdo->prepare(
        "SELECT * FROM jobs 
         WHERE status = 'pending' AND available_at <= NOW() 
         ORDER BY id ASC 
         LIMIT 1 FOR UPDATE"
    );
    $stmt->execute();
    $job = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if ($job) {
        // Помечаем как "в обработке"
        $updateStmt = $pdo->prepare("UPDATE jobs SET status = 'processing' WHERE id = ?");
        $updateStmt->execute([$job['id']]);
        
        try {
            // Выполнение задачи
            $data = json_decode($job['payload'], true);
            processJob($data);
            
            // Отмечаем как выполненную
            $pdo->prepare("UPDATE jobs SET status = 'completed' WHERE id = ?")
                ->execute([$job['id']]);
        } catch (Exception $e) {
            // Обработка ошибки
            $attempts = $job['attempts'] + 1;
            if ($attempts < 3) {
                // Повтор через 5 секунд
                $pdo->prepare("UPDATE jobs SET status = 'pending', attempts = ?, available_at = DATE_ADD(NOW(), INTERVAL 5 SECOND) WHERE id = ?")
                    ->execute([$attempts, $job['id']]);
            } else {
                $pdo->prepare("UPDATE jobs SET status = 'failed' WHERE id = ?")
                    ->execute([$job['id']]);
            }
        }
    } else {
        // Если задач нет — ждём 1 секунду
        sleep(1);
    }
}

Недостатки такого подхода:

  • Низкая производительность при большом количестве задач.
  • Риск блокировок таблицы при параллельных воркерах.
  • Отсутствие встроенных механизмов задержки и повторных попыток (всё нужно реализовывать вручную).

Продвинутая очередь с использованием Redis

Redis — это отличный выбор для организации очередей благодаря своей скорости и встроенным структурам данных, таким как списки (lists) и сортированные множества (sorted sets).

Установка: Убедитесь, что у вас установлен Redis и PHP-расширение phpredis или predis.

Пример с использованием списков (Lists):

// Подключение к Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// Добавление задачи в очередь
$redis->rPush('queue:emails', json_encode([
    'to' => 'user@example.com',
    'subject' => 'Привет из Redis!'
]));

// Воркер — извлечение задачи (блокирующая операция)
while (true) {
    // BLPOP блокирует выполнение, пока не появится задача
    $jobData = $redis->blPop('queue:emails', 0);
    
    if ($jobData) {
        $data = json_decode($jobData[1], true);
        try {
            processJob($data);
        } catch (Exception $e) {
            // Логирование ошибки
            error_log("Job failed: " . $e->getMessage());
        }
    }
}

Очередь с задержкой (Delayed Jobs):

Для задач, которые нужно выполнить не сразу, используйте сортированные множества:

// Добавление задачи с задержкой в 10 секунд
$delay = time() + 10;
$redis->zAdd('delayed:emails', $delay, json_encode([
    'to' => 'user@example.com',
    'subject' => 'Отложенное письмо'
]));

// Воркер, проверяющий отложенные задачи
while (true) {
    // Получаем задачи, время выполнения которых наступило
    $jobs = $redis->zRangeByScore('delayed:emails', 0, time());
    
    foreach ($                

Блоги

Рекомендаций по книгам