Очереди и фоновые задачи

Рейтинг: 60.1% · 14 голосов
Курс по Laravel: маршруты, Eloquent, Blade, миграции, очереди и API. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
oleg_php
Сообщения: 25
Зарегистрирован: 14 май 2026, 08:06

Очереди и фоновые задачи

Сообщение oleg_php »

АкадемияLaravel с нуляГлава 10 из 18
Оглавление курса (18)
  1. Знакомство с Laravel и установка окружения
  2. Маршруты и контроллеры
  3. Blade: шаблоны и вёрстка страниц
  4. Миграции и структура базы данных
  5. Eloquent ORM: модели и CRUD
  6. Связи в Eloquent: hasMany, belongsTo и другие
  7. Формы и валидация данных
  8. Аутентификация пользователей
  9. Middleware и защита маршрутов
  10. Очереди и фоновые задачи (вы здесь)
  11. Отправка почты и уведомления
  12. Строим REST API на Laravel
  13. Авторизация: Gates и Policies
  14. Работа с файлами: загрузка, Storage, диски local и S3
  15. Тестирование: Pest и PHPUnit, фабрики, сидеры, RefreshDatabase
  16. Сервис-контейнер, провайдеры, свои artisan-команды и планировщик
  17. Деплой в продакшен и обзор современного фронтенда (Vite, Livewire, Inertia)
  18. События и слушатели, кеширование, логирование
Отправка письма при регистрации или генерация отчёта на пару тысяч строк легко съедают 3-5 секунд, и всё это время человек смотрит на крутящийся спиннер. Очереди решают проблему: медленную работу мы откладываем в фон, ответ уходит мгновенно, а отдельный процесс разгребает накопившиеся задачи. В этой главе настроим очередь на драйвере database, напишем первую задачу и запустим воркер так, чтобы он пережил деплой.

Как это устроено:

Вместо того чтобы выполнять тяжёлый код прямо в HTTP-запросе, приложение кладёт в хранилище запись вида "выполни вот этот класс с такими параметрами". Отдельный процесс, воркер, забирает записи и выполняет их одну за другой. Хранилищем может быть таблица в базе, Redis или Amazon SQS. Для старта хватает обычной базы: начиная с Laravel 11 переменная QUEUE_CONNECTION в .env по умолчанию равна database, а миграции таблиц jobs и failed_jobs лежат в проекте из коробки, вы накатили их ещё в четвёртой главе вместе с остальными. Если у вас в .env стоит sync, задачи выполняются сразу, в том же запросе. Для отладки иногда удобно, но эффекта очереди вы не увидите.

Первая задача:

Возьмём для примера экспорт статей пользователя в CSV, операция заметно медленнее обычного запроса. Генерируем класс:

Код: Выделить всё

php artisan make:job ExportPostsReport
Файл появится в app/Jobs. Дописываем конструктор и логику:

Код: Выделить всё

<?php

namespace App\Jobs;

use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Storage;

class ExportPostsReport implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public int $timeout = 60;

    public function __construct(public User $user) {}

    public function handle(): void
    {
        $csv = $this->user->posts()
            ->orderBy('created_at')
            ->get()
            ->map(fn ($post) => implode(';', [$post->id, $post->title, $post->created_at]))
            ->implode("\n");

        Storage::put("reports/posts-{$this->user->id}.csv", $csv);
    }
}
Интерфейс ShouldQueue говорит фреймворку, что задачу надо класть в очередь, а не выполнять на месте. Свойство tries задаёт число попыток, timeout ограничивает длительность одной попытки в секундах. Трейт Queueable приносит dispatch, задержки, выбор очереди и сериализацию моделей. Метод posts() мы описали в главе про связи, здесь он работает как обычно.

Отправляем задачу в очередь из контроллера:

Код: Выделить всё

ExportPostsReport::dispatch($request->user());

// отложить на 10 минут
ExportPostsReport::dispatch($user)->delay(now()->addMinutes(10));

// отправить в отдельную очередь
ExportPostsReport::dispatch($user)->onQueue('reports');
После dispatch в таблице jobs появится строка, и больше ничего не произойдёт, пока не запущен воркер.

Запускаем воркер:

Код: Выделить всё

php artisan queue:work --tries=3 --timeout=90

# посмотреть упавшие задачи и перезапустить их
php artisan queue:failed
php artisan queue:retry all

# после каждого деплоя
php artisan queue:restart
queue:work поднимает приложение один раз и держит его в памяти, поэтому работает быстро, но не видит изменений кода. Для разработки есть queue:listen, он перезагружает приложение на каждой задаче, медленнее, зато код всегда свежий. Задача, упавшая больше tries раз, попадает в failed_jobs вместе с текстом исключения.

На проде воркер должен жить вечно и подниматься после падений. Стандарт здесь Supervisor, ставится через apt install supervisor, конфиг кладут в /etc/supervisor/conf.d/laravel-worker.conf:

Код: Выделить всё

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/blog/artisan queue:work --tries=3 --max-time=3600
numprocs=2
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/blog/storage/logs/worker.log
После правки конфига выполняем supervisorctl reread и supervisorctl update. Флаг max-time завершает воркер раз в час, supervisor тут же поднимает новый, это дешёвая защита от утечек памяти в долгоживущем PHP-процессе. На VPS за 300-400 рублей в месяц такая связка живёт годами.

Типичные грабли:

Первое место уверенно держит "задиспатчил, а ничего не происходит". Воркер просто не запущен, задачи копятся в jobs. Загляните в таблицу и в список процессов, прежде чем искать баг в коде.

Второе: после деплоя воркер продолжает выполнять старый код, потому что queue:work держит приложение в памяти. Лечится командой php artisan queue:restart, она через кэш просит воркеры мягко завершиться, а supervisor поднимает свежие. Добавьте её в деплой-скрипт один раз и забудьте.

Третье: сериализация моделей. В очередь уходит не вся модель, а только её id, воркер перед выполнением достаёт свежую запись из базы. Если запись к тому моменту удалили, получите ModelNotFoundException. По той же причине не передавайте в конструктор коллекции на тысячи моделей, передавайте id и выбирайте данные уже в handle().

Четвёртое: timeout должен быть меньше retry_after из config/queue.php (для database по умолчанию 90 секунд), иначе воркер решит, что зависшая задача потерялась, и она выполнится дважды. Для долгих задач поднимайте оба значения, а не только timeout.

Что усвоили:

Очередь это таблица с задачами плюс воркер, который их разгребает. Задача оформляется классом с ShouldQueue: конструктор принимает данные, handle() делает работу. На проде воркеры держит supervisor, после деплоя обязателен queue:restart, упавшие задачи живут в failed_jobs и перезапускаются через queue:retry. В следующей главе займёмся почтой, и очереди сразу пригодятся: письмо с ShouldQueue уходит в фон одной строчкой.
👍2 ❤️3 🔥3 😄 🤔3
✔ Лучший ответ сформирован автоматически — ngudyma
oleg_php писал(а):воркер решит, что зависшая задача потерялась, и она выполнится дважды поймал ровно это на проде. задача генерила pdf минуты три, retry_after стоял дефолтный 90, и клиентам улетало по два письма. поднял retry_after до 600, timeout до 540, полгода тишина. грабли реальные, не пропускайте этот абзац
Перейти к ответу →
Аватара пользователя
ngudyma
Сообщения: 2
Зарегистрирован: 28 май 2026, 06:45

Re: Очереди и фоновые задачи

Сообщение ngudyma »

✔ Лучший ответ — сформирован автоматически
oleg_php писал(а):воркер решит, что зависшая задача потерялась, и она выполнится дважды
поймал ровно это на проде. задача генерила pdf минуты три, retry_after стоял дефолтный 90, и клиентам улетало по два письма. поднял retry_after до 600, timeout до 540, полгода тишина. грабли реальные, не пропускайте этот абзац
👍1 ❤️2 🔥 😄 🤔1
Аватара пользователя
burnedsegfault
Сообщения: 2
Зарегистрирован: 05 июн 2026, 09:03

Re: Очереди и фоновые задачи

Сообщение burnedsegfault »

а если хостинг шаред и supervisor поставить нельзя? на том же бегете ssh есть, но своих демонов держать не дают. слышал про вариант с cron, который раз в минуту дергает queue:work --stop-when-empty, это нормальная практика или костыль?
👍2 ❤️ 🔥 😄 🤔
Аватара пользователя
vimops
Сообщения: 3
Зарегистрирован: 06 июн 2026, 17:12

Re: Очереди и фоновые задачи

Сообщение vimops »

правильно понимаю, что на локалке можно вообще не возиться с воркером, а поставить QUEUE_CONNECTION=sync и дебажить handle() как обычный код? или так можно прозевать проблемы с сериализацией, которые вылезут только на database?
👍1 ❤️2 🔥 😄 🤔
Аватара пользователя
bonda1
Сообщения: 2
Зарегистрирован: 13 май 2026, 13:19

Re: Очереди и фоновые задачи

Сообщение bonda1 »

спасибо за главу, наконец дошло, почему у нас после деплоя уведомления уходили со старыми текстами. неделю грешили на кэш блейда, а это воркер держал старый код в памяти. добавили queue:restart в деплой-скрипт и все починилось
👍1 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Middleware и защита маршрутов
Следующая глава →
Отправка почты и уведомления

Все главы курса «Laravel с нуля»

Поделиться темой: ✈ Telegram VK

Вернуться в «Laravel с нуля»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость