Как не дать вашему 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 выигрывает?
- Гарантированная доставка (ack/nack)