Очереди в Yii2: рабочие приёмы, чтобы не потерять задачи
Как устроен yii2-queue, чем отличаются драйверы, где задачи молча теряются и как настроить очередь так, чтобы воркер не падал на первой же необработанной ошибке.

Отправка письма прямо в контроллере — самая частая причина медленных ответов в Yii-приложениях. Пользователь ждёт, пока SMTP-сервер ответит, страница висит 3–5 секунд, а если письмо не ушло — валится весь запрос. Расширение yii2-queue убирает долгие операции из цикла запроса: контроллер кладёт задачу в очередь и сразу отвечает, а отдельный процесс-воркер разбирает задачи в фоне. Разберём, как подключить очередь на реальном проекте, какой драйвер выбрать и где чаще всего теряются задачи.
Материал для yii2-queue 2.3.x на Yii 2.0.4x. Пакет живёт отдельно от ядра фреймворка и ставится через Composer.
Что даёт очередь и чего от неё ждать не стоит
Очередь решает одну задачу: перенести выполнение кода из синхронного запроса в фоновый процесс. Письма, генерация PDF, обращения к внешним API, пересчёт агрегатов — всё, что можно сделать «потом», уходит в очередь.
Чего очередь не делает сама по себе: не гарантирует порядок выполнения между разными задачами, не защищает от повторного выполнения одной и той же задачи (об этом ниже) и не заменяет транзакции. Если задача упала на середине и часть побочных эффектов уже случилась — откатывать их придётся вручную.
Выбор драйвера: чем платите за каждый
yii2-queue поддерживает несколько бэкендов. Для большинства проектов выбор сводится к трём: db, redis и amqp (RabbitMQ). Разница не в скорости API, а в надёжности доставки и в том, что придётся администрировать.
| Драйвер | Что нужно | Гарантия доставки | Когда брать |
|---|---|---|---|
| db | Только БД (таблица queue) | Есть, задачи в транзакции с данными | Старт, невысокий поток, нет отдельной инфраструктуры |
| redis | Redis-сервер | Есть, но задача теряется, если Redis без персистентности | Много мелких задач, нужна скорость |
| amqp_interop | RabbitMQ + расширение | Подтверждения, повторы, приоритеты из коробки | Высокий поток, несколько типов задач, отдельная команда эксплуатации |
Ключевой момент про db: он позволяет положить задачу в ту же транзакцию, что и запись в основную таблицу. Заказ создан — задача на отправку письма гарантированно есть. Заказ откатился — задачи нет. С Redis такой связки нет: там задача существует отдельно от вашей БД.
Начинать почти всегда стоит с драйвера db. Он не требует ничего сверх того, что у вас уже есть, и переезд на Redis или RabbitMQ позже — это смена конфигурации, а не переписывание задач.
Подключение драйвера db
Установка пакета:
composer require yiisoft/yii2-queue
Для драйвера db нужна таблица. Миграция входит в пакет — применяется отдельной командой с указанием пути к миграциям расширения:
php yii migrate --migrationPath=@yii/queue/db/migrations
После этого в БД появится таблица queue с полями id, channel, job, reserved_at, done_at. Если таблица не появилась — проверьте, что миграция применилась именно к нужному соединению db.
Конфигурация компонента в config/main.php (или в консольном конфиге — очередь нужна и там, и там):
'components' => [
'queue' => [
'class' => \yii\queue\db\Queue::class,
'db' => 'db', // ID компонента подключения к БД
'tableName' => '{{%queue}}',
'channel' => 'default',
'mutex' => \yii\mutex\MysqlMutex::class,
'ttr' => 5 * 60, // время на выполнение задачи, секунды
'attempts' => 3, // сколько раз повторить при падении
],
],
Параметр ttr (time to reserve) — сколько секунд задача считается «взятой в работу». Если воркер за это время не сообщил об успехе, задача вернётся в очередь и будет взята снова. Ставьте с запасом относительно реального времени выполнения, иначе получите ложные повторы.
Задача как отдельный класс
Задача — это класс, реализующий yii\queue\JobInterface с методом execute(). Все данные, нужные для выполнения, передаются через публичные свойства и сериализуются вместе с объектом.
<?php
namespace app\jobs;
use Yii;
use yii\base\BaseObject;
use yii\queue\JobInterface;
use yii\queue\Queue;
class SendOrderEmailJob extends BaseObject implements JobInterface
{
// Передаём только ID, а не весь объект заказа
public int $orderId;
public function execute($queue): void
{
$order = \app\models\Order::findOne($this->orderId);
if ($order === null) {
// Заказ удалили, пока задача ждала — тихо выходим
return;
}
Yii::$app->mailer->compose('order-confirm', ['order' => $order])
->setTo($order->email)
->setSubject('Ваш заказ №' . $order->id)
->send();
}
}
Обратите внимание на первое правило: в задачу передаётся ID, а не сам объект модели. Если положить в свойство public Order $order, в очередь уедет сериализованная копия со всеми связями на момент постановки. Когда воркер её достанет, данные уже могут быть устаревшими, а сериализация ActiveRecord с реляциями раздувает запись и ломается на замыканиях.
Постановка задачи в очередь из контроллера:
Yii::$app->queue->push(new \app\jobs\SendOrderEmailJob([
'orderId' => $order->id,
]));
Запуск воркера: не через cron каждую минуту
Здесь новички чаще всего делают неправильно. Есть две команды:
php yii queue/run— берёт все задачи, что есть сейчас, выполняет и завершается. Годится для запуска по cron, но между запусками задачи ждут.php yii queue/listen— висит постоянно, разбирает задачи по мере поступления. Это то, что нужно в проде.
Держать queue/listen живым должен супервизор, а не cron. Процесс упадёт (память, необработанное исключение, деплой) — супервизор поднимет его снова. Пример секции для supervisord:
[program:yii-queue]
command=php /var/www/app/yii queue/listen --verbose=1
numprocs=1
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/yii-queue.log
Флаг --verbose=1 пишет в лог, какая задача взята и чем закончилась — без него отладка превращается в гадание.
Почему не «cron каждую минуту с queue/run»
Работает до первого всплеска. Если задач накопилось на 5 минут, а cron запускает queue/run ежеминутно, вы получите несколько параллельных процессов, которые могут взяться за одни и те же задачи. Мьютекс в конфиге частично это разруливает, но правильнее — один долгоживущий listener под супервизором.
Идемпотентность: задача выполнится больше одного раза
Это не «может быть», а «рано или поздно случится». Воркер взял задачу, отправил письмо, но упал до того, как отметил её выполненной — при ttr задача вернётся и письмо уйдёт повторно. Отправка письма дважды — терпимо. Повторное списание денег или создание дубля заказа — нет.
Правило: задача должна быть безопасна при повторном запуске. Перед действием с побочным эффектом проверяйте, не сделано ли оно уже:
public function execute($queue): void
{
$order = \app\models\Order::findOne($this->orderId);
if ($order === null || $order->email_sent_at !== null) {
return; // Уже отправлено — повтор ничего не делает
}
// ... отправка ...
$order->email_sent_at = date('Y-m-d H:i:s');
$order->save(false, ['email_sent_at']);
}
Что ломается на практике
- Воркер не видит новый код после деплоя. Долгоживущий процесс держит старые классы в памяти. После выкатки перезапускайте listener:
supervisorctl restart yii-queue. Забыли — задачи выполняются старым кодом. - Задача молча падает без следов. Исключение внутри
execute()уходит в лог воркера, а не в лог веб-приложения. Смотритеstdout_logfileсупервизора, а не толькоruntime/logs/app.log. - Утечка соединения с БД. Долгий listener держит одно подключение. При драйвере db на MySQL с коротким
wait_timeoutсоединение отваливается, и задачи перестают браться. Лечится настройкой переподключения или периодическим перезапуском воркера супервизором. - Растущая таблица queue. Драйвер db не чистит выполненные задачи автоматически в старых версиях. Проверьте, накапливаются ли строки с заполненным
done_at, и удаляйте их по расписанию, если растут. - Раздутая сериализация. Признак — большие значения в поле
job. Значит, в задачу кладут модели целиком вместо ID.
Куда смотреть дальше
Настроили db-драйвер и listener под супервизором — дальше по мере роста нагрузки смотрите на две вещи. Первое: количество задач со статусом «взята, но не завершена» (заполнен reserved_at, пуст done_at) — это ваши подвисшие или падающие задачи. Второе: задержку между push и выполнением — если она растёт, добавляйте воркеры (numprocs в супервизоре) или переезжайте с db на Redis/RabbitMQ.
Приоритеты задач, отложенный запуск через delay() и работа с несколькими каналами — отдельные темы, здесь мы разобрали только базовую надёжную конфигурацию. За полным списком опций драйверов идите в официальную документацию расширения.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: yii2-queue — официальная документация (GitHub)
Частые вопросы
У меня Yii Basic, а не Advanced — как настроить очередь?
Задача выполняется дважды, хотя я нажал отправку один раз — это баг?
Что будет с задачами, если воркер вообще не запущен?
Почему после деплоя задачи выполняются старым кодом?
Можно ли запускать queue/listen напрямую без супервизора?
Как отлаживать задачу, которая не выполняется?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.