Очереди и фоновые задачи на PHP: как ускорить сайт и не потерять данные

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

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

Начать курс

Как не дать вашему PHP-сайту «зависнуть» на долгих операциях?

Вы когда-нибудь ждали 30 секунд, пока сайт отправит email с подтверждением? Или наблюдали, как страница «подвисает» при генерации PDF-отчета? В мире PHP, где каждый запрос обычно выполняется синхронно, такие задачи могут убить пользовательский опыт. Но есть проверенное решение — очереди и фоновые задачи. Они позволяют перенести «тяжелую» работу за кулисы, а пользователю мгновенно показать: «Ваш запрос принят!».

В этой статье мы разберем, как организовать очереди на PHP с помощью популярных инструментов: RabbitMQ, Redis и Gearman. Вы увидите реальные примеры кода, узнаете, как избежать типичных ошибок, и сможете внедрить фоновую обработку на своем проекте уже сегодня.

Зачем нужны очереди в PHP?

PHP традиционно работает в модели «запрос-ответ». Но когда приходит время для операций, которые длятся дольше 1-2 секунд (отправка email, обработка изображений, генерация отчетов, вебхуки), пользователь сталкивается с тайм-аутами или просто скучает, глядя на spinner. Очереди решают эту проблему кардинально:

  • Мгновенный отклик — пользователь сразу получает ответ, а тяжелая задача уходит в фон.
  • Масштабирование — вы можете запускать десятки воркеров параллельно, не меняя код.
  • Надежность — если воркер «упал», задача останется в очереди и будет выполнена позже.
  • Балансировка нагрузки — пиковые нагрузки сглаживаются за счет буферизации.

Давайте посмотрим, как это реализовать на практике, используя три самых популярных подхода.

1. Очереди на Redis: легко и быстро

Redis — это не просто кэш. Благодаря структурам данных (списки, множества) и механизму Pub/Sub, он идеально подходит для простых очередей. Если у вас уже есть Redis на сервере, это самый быстрый способ внедрить фоновые задачи без установки дополнительных сервисов.

Пример: очередь email-рассылки с Redis

Предположим, после регистрации пользователя нужно отправить приветственное письмо. Вместо того чтобы вызывать mail() внутри контроллера, мы просто кладем задачу в очередь.

Шаг 1. Добавляем задачу в очередь (продюсер):

<?php
// producer.php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$task = json_encode([
    'type' => 'welcome_email',
    'user_id' => 42,
    'email' => 'user@example.com',
    'created_at' => time()
]);

// RPUSH добавляет элемент в конец списка
$redis->rPush('email_queue', $task);
echo "Задача добавлена в очередь\n";
?>

Шаг 2. Воркер, который обрабатывает задачи (консьюмер):

<?php
// worker.php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

echo "Воркер запущен, ожидаю задачи...\n";

while (true) {
    // BLPop блокирует выполнение, пока не появится элемент
    $taskData = $redis->blPop('email_queue', 5); // таймаут 5 сек
    
    if ($taskData) {
        $task = json_decode($taskData[1], true);
        
        echo "Обрабатываю задачу: {$task['type']} для {$task['email']}\n";
        
        // Имитация отправки письма
        sleep(2);
        
        // Здесь реальная логика: mail(), SMTP и т.д.
        echo "Письмо отправлено пользователю {$task['user_id']}\n";
    }
}
?>

Запуск:

Воркер запускается в фоне (через nohup или Supervisor):

php worker.php &

Теперь, когда пользователь регистрируется, контроллер вызывает producer.php и сразу возвращает ответ, а воркер спокойно обрабатывает письма. Redis гарантирует, что задача не потеряется, даже если воркер временно упадет.

2. RabbitMQ: промышленный стандарт

Если ваш проект растет и требуется маршрутизация, подтверждения, dead-letter очереди — Redis может не справиться. Тут на сцену выходит RabbitMQ. Это полноценный брокер сообщений с поддержкой протокола AMQP. Он используется в высоконагруженных системах (например, в самом PythonLib для обработки аналитики).

Установка и подключение

Для работы с RabbitMQ в PHP используйте библиотеку php-amqplib:

composer require php-amqplib/php-amqplib

Пример: генерация PDF-отчета в фоне

Продюсер (отправляем задачу):

<?php
require_once 'vendor/autoload.php';

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->queue_declare('pdf_generation', false, true, false, false); // durable = true

$data = json_encode([
    'report_id' => 123,
    'user_id' => 7,
    'format' => 'pdf'
]);

$msg = new AMQPMessage($data, ['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]);
$channel->basic_publish($msg, '', 'pdf_generation');

echo "Задача на генерацию PDF отправлена\n";

$channel->close();
$connection->close();
?>

Консьюмер (воркер):

<?php
require_once 'vendor/autoload.php';

use PhpAmqpLib\Connection\AMQPStreamConnection;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$channel->queue_declare('pdf_generation', false, true, false, false);

echo "Ожидаю задачи на генерацию PDF...\n";

$callback = function ($msg) {
    $task = json_decode($msg->body, true);
    echo "Генерирую отчет #{$task['report_id']} для пользователя {$task['user_id']}\n";
    
    // Имитация генерации PDF
    sleep(5);
    
    echo "PDF готов!\n";
    $msg->ack(); // подтверждаем успешную обработку
};

$channel->basic_qos(null, 1, null); // не брать новую задачу, пока не обработана текущая
$channel->basic_consume('pdf_generation', '', false, false, false, false, $callback);

while ($channel->is_consuming()) {
    $channel->wait();
}
?>

Почему RabbitMQ выигрывает?