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

Отправка письма прямо в контроллере — самая частая причина, по которой оформление заказа в интернет-магазине на Yii2 тормозит на две-три секунды. SMTP-сервер отвечает медленно, а пользователь смотрит на крутилку. Решение известно: вынести письмо в фоновую задачу через расширение yiisoft/yii2-queue. Но между «поставил задачу в очередь» и «очередь работает в проде без сюрпризов» лежит десяток граблей: задачи теряются при деплое, выполняются дважды, воркер молча падает и не поднимается. Разберём приёмы, которые снимают эти проблемы.
Речь про yii2-queue версии 2.3.x на Yii2 2.0.x. Драйверы разные — Redis, RabbitMQ, обычная БД — приёмы по большей части общие, различия отмечаю отдельно.
Базовая настройка, которую потом не переделывать
Компонент очереди подключается в конфиге приложения. Для консольного и веб-приложения он должен быть одинаковым — иначе задача, поставленная из веб-части, не найдётся воркером. Держите настройку очереди в общем конфиге (config/common.php в advanced-шаблоне), а не в web/console по отдельности.
// config/common.php
'bootstrap' => ['queue'], // регистрирует консольный контроллер и обработчики
'components' => [
'queue' => [
'class' => \yii\queue\redis\Queue::class,
'redis' => 'redis', // id компонента-соединения
'retry' => 3, // сколько раз повторить упавшую задачу
'ttr' => 5 * 60, // time to reserve: сколько секунд задаче на выполнение
],
]
Два параметра здесь решают судьбу очереди в проде. ttr — сколько времени задаче отводится на выполнение, прежде чем очередь сочтёт её зависшей и вернёт обратно. retry — сколько раз повторять упавшую задачу. Значения по умолчанию (ttr 300, retry 1) для тяжёлых задач малы: если генерация PDF длится дольше пяти минут, задача уйдёт на повтор, и вы получите два одинаковых документа.
Задача — это класс, а не замыкание
Соблазн передать в очередь замыкание есть, но yii2-queue сериализует задачу целиком, а замыкание с захваченным контекстом не сериализуется предсказуемо. Задача — отдельный класс, реализующий yii\queue\JobInterface. Всё, что нужно для выполнения, кладите в публичные свойства.
use yii\base\BaseObject;
use yii\queue\JobInterface;
class SendOrderEmailJob extends BaseObject implements JobInterface
{
public int $orderId; // храним id, а не сам объект заказа
public function execute($queue)
{
$order = \app\models\Order::findOne($this->orderId);
if ($order === null) {
// заказ удалили, пока задача ждала — молча выходим
return;
}
Yii::$app->mailer->compose('orderConfirm', ['order' => $order])
->setTo($order->email)
->send();
}
}
Ключевой приём в этом фрагменте — в свойстве лежит orderId, а не сам объект Order. Задача может пролежать в очереди минуты. За это время объект в памяти устареет: цену изменили, заказ отменили. Восстанавливать актуальное состояние из БД внутри execute — правило, а не рекомендация.
Идемпотентность: задача выполнится дважды, смиритесь
При retry больше единицы и при внезапном перезапуске воркера одна и та же задача может отработать повторно. Это не баг очереди, а её нормальное свойство — гарантия «доставим хотя бы один раз», а не «ровно один раз». Если ваша задача списывает деньги или начисляет бонусы, двойное выполнение обернётся реальной потерей.
Проектируйте каждую задачу так, будто её выполнят дважды подряд. Если повтор безопасен — очередь надёжна. Если нет — вы отлаживаете плавающий баг раз в месяц по логам.
Практический приём — флаг завершённости на самой сущности. Перед действием проверяете, не сделано ли оно уже:
public function execute($queue)
{
$order = Order::findOne($this->orderId);
if ($order === null || $order->email_sent_at !== null) {
return; // письмо уже отправлено, повтор игнорируем
}
// отправляем письмо
$order->updateAttributes(['email_sent_at' => new \yii\db\Expression('NOW()')]);
}
Для операций с деньгами флага мало — нужна транзакция и уникальный ключ операции в таблице проводок, чтобы повторная вставка упала на уникальном индексе. Проектирование денежной идемпотентности — отдельная тема, здесь фиксирую только принцип: полагаться на «ну она же выполнится один раз» нельзя.
Деплой не должен терять задачи
Воркер запускается командой yii queue/listen (постоянный процесс) или yii queue/run (обработать текущие и выйти). Проблема с listen: при деплое вы перезапускаете процесс, и если он в этот момент выполняет задачу — она обрывается на полуслове.
Два приёма против этого.
- Запускайте воркер под supervisor, а не через
nohup. Supervisor посылает процессуSIGTERM, аyii2-queueобрабатывает сигнал: доделывает текущую задачу и только потом выходит. Дайте supervisor достаточныйstopwaitsecs— не меньше вашегоttr, иначе он прибьёт воркер на середине. - Не держите долгоживущий процесс без ограничений. PHP накапливает утечки памяти. Флаг
--isolateуlistenвыполняет каждую задачу в отдельном дочернем процессе — память чистая, но чуть медленнее из-за форка. Для тяжёлых задач это выгодная сделка.
Пример секции supervisor:
[program:yii-queue]
command=php /var/www/app/yii queue/listen --verbose=1 --color=0
numprocs=2
process_name=%(program_name)s_%(process_num)02d
stopwaitsecs=310
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/yii-queue.log
stopwaitsecs=310 здесь чуть больше ttr из конфига (300 секунд) — запас, чтобы задача успела завершиться при рестарте.
Выбор драйвера под нагрузку
Драйвер БД (yii\queue\db\Queue) удобен на старте: ничего не надо ставить, задачи видно в таблице. Но каждый воркер опрашивает таблицу SELECT ... FOR UPDATE, и на десятках задач в секунду это создаёт блокировки. Ниже — ориентиры, не жёсткие границы.
| Драйвер | Когда брать | Чем платите |
|---|---|---|
| db | Старт, единицы задач в минуту, нужна видимость в БД | Блокировки таблицы под нагрузкой, ручная чистка старых задач |
| redis | Десятки-сотни задач в секунду, уже есть Redis | Нет отложенной задержки из коробки в старых версиях, задачи в памяти — при падении Redis без персистентности теряются |
| amqp (RabbitMQ) | Тысячи задач, приоритеты, разные обработчики, гарантии доставки | Отдельный сервис в инфраструктуре, порог входа выше |
Практика: начинайте с db, пока задачи редкие, и переезжайте на Redis, когда таблица очереди начнёт тормозить. Amqp берите осознанно — под приоритеты и несколько типов обработчиков, а не потому что «серьёзно».
Отложенные задачи и приоритет
Напоминание через час, повторная попытка платежа через пять минут — это отложенный запуск. Задаётся через delay при постановке:
Yii::$app->queue->delay(5 * 60)->push(new RetryPaymentJob([
'paymentId' => $payment->id,
]));
Приоритет (priority) поддерживают не все драйверы — у db и amqp есть, у Redis-драйвера с ним сложнее. Если приоритет критичен, проверяйте поддержку в конкретной версии драйвера по документации расширения, а не полагайтесь на то, что метод просто есть в интерфейсе.
Если не сработало
- Задача поставлена, но воркер её не берёт. Почти всегда — разные конфиги очереди в web и console. Убедитесь, что компонент
queueобъявлен в общем конфиге и подключён в оба приложения. - Задача падает с
Class not foundв воркере. Воркер — отдельный процесс, поднятый до вашего деплоя. После обновления кода перезапустите воркер (supervisorctl restart yii-queue:*), иначе он крутит старый код. - Задачи выполняются, но по одной и медленно. Один воркер обрабатывает задачи последовательно. Поднимите несколько процессов (
numprocsв supervisor) — они разберут очередь параллельно, если драйвер это позволяет. - Redis-очередь молча теряет задачи после перезагрузки сервера. Проверьте персистентность Redis (
appendonlyили снапшоты). По умолчанию всё в памяти. - Задача бесконечно повторяется. Внутри падает исключение,
retryотрабатывает, задача возвращается. Логируйте текст исключения изexecuteи настройте обработчик событияQueue::EVENT_AFTER_ERROR, чтобы видеть, что именно ломается.
Что настроить дальше
Первое после запуска — мониторинг длины очереди. Если число необработанных задач растёт, воркеры не справляются: добавьте процессы или разнесите тяжёлые задачи в отдельную очередь. Второе — алерт на попадание задач в «мёртвую» очередь после исчерпания retry, чтобы упавшие письма и платежи не терялись молча. Смотрите в проде на два показателя: время нахождения задачи в очереди до старта и долю задач, ушедших на повтор. Оба должны стремиться к нулю в спокойном режиме.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: yii2-queue — официальный репозиторий и документация, Yii2 Guide — Queue
Частые вопросы
У меня Yii2 2.0.15 и старая версия расширения — приёмы подойдут?
Что делать, если воркер после деплоя выполняет старый код?
Задача выполнилась дважды и списала бонусы два раза — это баг очереди?
Redis или БД — что выбрать для магазина с сотней заказов в день?
Почему нельзя передать в задачу сам объект модели вместо id?
Как понять, что воркеры не справляются с потоком задач?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.