Представь: ты запускаешь сайт, а он начинает тормозить. Пользователи жалуются, что письма приходят через час, а картинки загружаются полдня. Знакомая боль? Чаще всего проблема в том, что PHP делает всё сразу: и письма шлёт, и картинки ресайзит, и данные обрабатывает. А нужно просто разделить задачи. Вот тут и приходят на помощь очереди. В этой статье я расскажу, как очереди спасают производительность, и покажу на примерах, как их внедрить в PHP-приложение.

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

Очередь — это как стопка тарелок в ресторане. Повар готовит блюда, официант забирает их по очереди. В программировании то же самое: ты кладёшь задачу в очередь, а специальный обработчик (воркер) забирает её и выполняет. При этом PHP-скрипт, который обрабатывает запрос пользователя, не ждёт завершения задачи. Он просто кладёт её в очередь и идёт дальше.

Зачем это нужно? Представь, что пользователь регистрируется на сайте. Нужно отправить письмо, создать папку для файлов, записать в лог. Если делать всё сразу, ответ пользователю придёт через 10 секунд. А если отправить задачу в очередь — ответ придёт мгновенно, а письмо уйдёт через пару секунд.

Очереди незаменимы в таких сценариях:

  • Отправка email-уведомлений
  • Обработка изображений (ресайз, сжатие)
  • Генерация отчётов
  • Импорт больших файлов
  • Взаимодействие с внешними API

Основные компоненты очереди

Любая очередь состоит из трёх частей:

  • Продюсер — это твой PHP-код, который кладёт задачу в очередь.
  • Брокер сообщений — сервер, который хранит задачи. Например, RabbitMQ, Redis или база данных.
  • Консюмер (воркер) — скрипт, который забирает задачи и выполняет их.

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

Как это работает на практике: пример с Redis

Давай посмотрим на простой пример. Допустим, нужно отправлять письма после регистрации. Мы будем использовать библиотеку predis/predis для работы с Redis.

Сначала устанавливаем библиотеку через Composer:

composer require predis/predis

Теперь напишем продюсера — код, который добавляет задачу в очередь:

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

$client = new Predis\Client([
    'scheme' => 'tcp',
    'host'   => '127.0.0.1',
    'port'   => 6379,
]);

// Данные для письма
$emailData = [
    'to' => 'user@example.com',
    'subject' => 'Добро пожаловать!',
    'body' => 'Спасибо за регистрацию.',
];

// Добавляем задачу в список Redis
$client->rpush('email_queue', json_encode($emailData));

echo 'Задача добавлена в очередь.';

Этот код просто кладёт данные в список Redis. Теперь напишем консюмера — скрипт, который будет забирать задачи и отправлять письма:

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

$client = new Predis\Client([
    'scheme' => 'tcp',
    'host'   => '127.0.0.1',
    'port'   => 6379,
]);

while (true) {
    // Ждём задачу из списка (блокирующий вызов)
    $task = $client->blpop('email_queue', 0);

    if ($task) {
        $emailData = json_decode($task[1], true);
        
        // Здесь должна быть функция отправки письма
        // mail($emailData['to'], $emailData['subject'], $emailData['body']);
        
        echo 'Отправлено письмо на ' . $emailData['to'] . PHP_EOL;
    }
}

Этот скрипт запускается в фоне и бесконечно проверяет очередь. Как только появляется задача — он её выполняет. Такой подход называется «воркер». Его можно запустить через командную строку: php worker.php.

Продвинутый вариант: RabbitMQ и библиотека php-amqplib

Redis — это хорошо для простых задач. Но если нужно управлять маршрутизацией, подтверждениями или настраивать обменники, лучше взять RabbitMQ. Он сложнее, но гибче.

Установим библиотеку:

composer require php-amqplib/php-amqplib

Продюсер на RabbitMQ:

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

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

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

// Объявляем очередь
$channel->queue_declare('task_queue', false, true, false, false);

$data = json_encode([
    'user_id' => 123,
    'action' => 'send_welcome_email',
]);

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

echo 'Задача отправлена в RabbitMQ.';

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

Консюмер для RabbitMQ:

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

use PhpAmqpLib\Connection\AMQPStreamConnection;

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

$channel->queue_declare('task_queue', false, true, false, false);

echo 'Ожидание задач...';

$callback = function ($msg) {
    $data = json_decode($msg->body, true);
    echo 'Обрабатываю задачу для user_id: ' . $data['user_id'] . PHP_EOL;
    
    // Подтверждаем выполнение
    $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
};

$channel->basic_qos(null, 1, null);
$channel->basic_consume('task_queue', '', false, false, false, false, $callback);

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

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

Разница в том, что RabbitMQ гарантирует доставку даже при падении сервера. И поддерживает сложные маршруты.

Правило: Если терять задачи нельзя — используй RabbitMQ с подтверждениями. Если скорость важнее надёжности — бери Redis. Для большинства веб-проектов Redis достаточно.

Сравнение популярных брокеров для PHP

БрокерСкоростьНадёжностьСложность настройкиПопулярность в PHP
Redis (списки)ВысокаяСредняяНизкаяОчень высокая
RabbitMQСредняяВысокаяСредняяВысокая
BeanstalkdВысокаяСредняяНизкаяСредняя
БД (MySQL, PostgreSQL)НизкаяВысокаяНизкаяСредняя

Лайфхаки для работы с очередями

  • Обрабатывай ошибки. Если задача упала, не теряй её. Используй механизм повторных попыток (retry). В RabbitMQ для этого есть dead letter queues.
  • Не делай задачи слишком маленькими. Если каждая задача выполняется меньше секунды, накладные расходы на очередь будут больше пользы. Группируй.
  • Мониторь очередь. Ставь метрики: количество задач, время выполнения. Иначе однажды очередь завалится, а ты узнаешь об этом от пользователей.
  • Используй супервизор. Запускай воркеры через supervisor или systemd. Если воркер упадёт — он перезапустится автоматически.
Лайфхак: Если используешь Redis, не забывай про BRPOPLPUSH. Он атомарно переносит задачу в отдельный список для обработки. Если воркер упадёт, задача не потеряется — она останется во втором списке.

Что в итоге

Очереди — это не rocket science. Это просто способ не делать всё сразу. PHP отлично с ними дружит, если использовать правильные инструменты. Начни с Redis и простого продюсера-консюмера. Когда проект вырастет — перейдёшь на RabbitMQ. Главное — не бойся очередей. Они сэкономят тебе кучу нервов и времени.

Студия WNDER