Kolejki i zadania w Laravel: architektura asynchroniczna i pytania rekrutacyjne 2026

Dogłębne omówienie architektury kolejek i zadań w Laravel. Obejmuje dispatching, batching, łańcuchowanie, middleware, obsługę nieudanych zadań i zarządzanie workerami z przykładami Laravel 12.

Diagram architektury asynchronicznej kolejek i zadań Laravel z procesami workerów i pipeline dispatchingu zadań

Kolejki w Laravel oferują ujednolicone API do odraczania czasochłonnych zadań — wysyłki maili, przetwarzania uploadów, generowania raportów — do procesów wykonywanych w tle. Zamiast zmuszać użytkowników do oczekiwania, aplikacja wrzuca zadanie do kolejki i kontynuuje pracę. Mechanizm ten stanowi rdzeń każdej skalowalnej aplikacji Laravel.

Architektura kolejek w skrócie

Laravel obsługuje wiele backendów kolejek (Redis, Amazon SQS, baza danych, Beanstalkd) poprzez jedno, niezależne od sterownika API. Zadania to serializowane klasy PHP implementujące interfejs ShouldQueue. Workery pobierają zadania z kolejki, deserializują je i wykonują metodę handle(). Nieudane zadania trafiają do dedykowanej tabeli failed_jobs, gdzie czekają na ponowienie lub analizę.

Jak działa dispatching zadań w Laravel pod maską

Wywołanie dispatch() na klasie zadania powoduje, że Laravel serializuje instancję — wraz z jej publicznymi właściwościami — i wrzuca payload do skonfigurowanego połączenia kolejki. Serializowany payload zawiera w pełni kwalifikowaną nazwę klasy, zserializowane właściwości, nazwę docelowej kolejki oraz metadane, takie jak liczba dozwolonych prób i timeout.

Proces workera (php artisan queue:work) działa jako długo żyjący demon, który odpytuje backend kolejki w poszukiwaniu nowych zadań. Po odebraniu payloadu worker deserializuje zadanie, rozwiązuje jego zależności poprzez kontener usług i wywołuje handle().

App/Jobs/ProcessInvoice.phpphp
namespace App\Jobs;

use App\Models\Order;
use App\Services\PdfGenerator;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ProcessInvoice implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

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

    public function __construct(
        public readonly Order $order
    ) {}

    public function handle(PdfGenerator $pdf): void
    {
        // Generate PDF invoice for the order
        $invoice = $pdf->generate($this->order);

        // Store the generated file
        $this->order->update([
            'invoice_path' => $invoice->path(),
            'invoiced_at' => now(),
        ]);
    }

    public function failed(\Throwable $e): void
    {
        // Notify the ops team when invoice generation fails
        logger()->error('Invoice generation failed', [
            'order_id' => $this->order->id,
            'error' => $e->getMessage(),
        ]);
    }
}

Trait SerializesModels przechowuje wyłącznie klucz główny modelu i jego nazwę klasy, a nie cały model Eloquent. Gdy worker przetwarza zadanie, pobiera świeży model z bazy danych. Takie podejście zapobiega pracy na nieaktualnych danych i utrzymuje rozmiar payloadu na niskim poziomie.

Batching zadań dla równoległych obciążeń

Batching grupuje wiele zadań w jeden batch, śledzi ich łączny postęp i wyzwala callbacki po zakończeniu wszystkich zadań — albo gdy którekolwiek z nich zakończy się błędem. Ten wzorzec sprawdza się przy importach danych, masowych powiadomieniach i generowaniu raportów, gdzie kilka niezależnych jednostek pracy musi się zakończyć zanim wystartuje krok finalny.

App/Http/Controllers/ImportController.phpphp
use App\Jobs\ImportRow;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;

public function import(Request $request)
{
    $rows = $this->parseCSV($request->file('data'));

    // Create a batch of import jobs, one per CSV row
    $batch = Bus::batch(
        collect($rows)->map(fn ($row) => new ImportRow($row))
    )
    ->then(function (Batch $batch) {
        // All jobs completed successfully
        Notification::send(
            auth()->user(),
            new ImportComplete($batch->totalJobs)
        );
    })
    ->catch(function (Batch $batch, \Throwable $e) {
        // First failure in the batch
        logger()->warning('Batch import partial failure', [
            'batch_id' => $batch->id,
            'failed' => $batch->failedJobs,
        ]);
    })
    ->finally(function (Batch $batch) {
        // Runs after all jobs finish (success or failure)
        Cache::forget("import_lock_{$batch->id}");
    })
    ->allowFailures()
    ->dispatch();

    return response()->json(['batch_id' => $batch->id]);
}

Laravel 12 wzbogaca payloady batchy o metadane, w tym czas oczekiwania w kolejce i identyfikację workera. Metoda allowFailures() zapobiega anulowaniu całego batcha przez pojedyncze nieudane zadanie — kluczowe przy dużych importach, gdzie częściowy sukces jest akceptowalny.

Łańcuchy zadań dla sekwencyjnych przepływów

Podczas gdy batching obsługuje obciążenia równoległe, łańcuchowanie gwarantuje wykonanie sekwencyjne. Każde zadanie w łańcuchu uruchamia się dopiero po pomyślnym zakończeniu poprzedniego. W razie błędu pozostała część łańcucha zostaje porzucona, a callback catch zostaje uruchomiony.

App/Services/OrderWorkflow.phpphp
use App\Jobs\ValidatePayment;
use App\Jobs\ReserveInventory;
use App\Jobs\SendConfirmation;
use App\Jobs\GenerateShippingLabel;
use Illuminate\Support\Facades\Bus;

public function processOrder(Order $order): void
{
    // Each job runs only after the previous one succeeds
    Bus::chain([
        new ValidatePayment($order),
        new ReserveInventory($order),
        new GenerateShippingLabel($order),
        new SendConfirmation($order),
    ])
    ->onQueue('orders')
    ->catch(function (\Throwable $e) use ($order) {
        // Roll back the order if any step fails
        $order->update(['status' => 'failed']);
        logger()->error('Order chain failed', [
            'order_id' => $order->id,
            'step' => $e->getMessage(),
        ]);
    })
    ->dispatch();
}

Łańcuchowanie wyróżnia się w przepływach domenowych, w których kolejność kroków ma znaczenie — płatność musi zostać zwalidowana zanim zarezerwowany zostanie magazyn, a etykiety wysyłkowe zależą od potwierdzonego stanu magazynowego.

Gotowy na rozmowy o Laravel?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Middleware kolejek dla zagadnień przekrojowych

Middleware kolejek otacza wykonanie zadania reużywalną logiką: rate limiting, deduplikacja, circuit breaker. Zamiast wbijać te zagadnienia w każde zadanie, middleware utrzymuje zadania skupione na logice biznesowej.

App/Jobs/Middleware/RateLimitedJob.phpphp
namespace App\Jobs\Middleware;

use Closure;
use Illuminate\Support\Facades\RateLimiter;

class RateLimitedJob
{
    public function __construct(
        private string $key,
        private int $maxAttempts = 10,
        private int $decaySeconds = 60
    ) {}

    public function handle(object $job, Closure $next): void
    {
        // Release job back to queue if rate limit exceeded
        if (RateLimiter::tooManyAttempts($this->key, $this->maxAttempts)) {
            $job->release($this->decaySeconds);
            return;
        }

        RateLimiter::hit($this->key, $this->decaySeconds);

        $next($job);
    }
}

Middleware podpina się poprzez zdefiniowanie metody middleware() na klasie zadania:

App/Jobs/CallExternalApi.phpphp
public function middleware(): array
{
    return [
        new RateLimitedJob(
            key: 'external-api',
            maxAttempts: 30,
            decaySeconds: 60
        ),
        // Prevent duplicate jobs from running concurrently
        (new WithoutOverlapping($this->apiResource->id))
            ->releaseAfter(300)
            ->expireAfter(600),
    ];
}

Middleware WithoutOverlapping wykorzystuje atomowe locki, aby tylko jedna instancja zadania (identyfikowana po kluczu) działała w danym momencie. W połączeniu z rate limitingiem zapobiega zarówno duplikacji przetwarzania, jak i throttlingowi po stronie API.

Obsługa nieudanych zadań i strategie ponawiania

Produkcyjne systemy kolejek wymagają solidnej obsługi błędów. Laravel zapisuje nieudane zadania w tabeli failed_jobs razem z pełnym payloadem, śladem wyjątku oraz informacją o kolejce/połączeniu, które wytworzyło błąd. Metoda failed() na klasie zadania uruchamia się po wyczerpaniu wszystkich prób.

Konfigurowanie zachowania ponawiania per zadanie daje precyzyjną kontrolę:

App/Jobs/SyncExternalData.phpphp
class SyncExternalData implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 5;

    // Exponential backoff: 10s, 30s, 60s, 120s, 300s
    public function backoff(): array
    {
        return [10, 30, 60, 120, 300];
    }

    // Job-specific timeout
    public int $timeout = 180;

    // Maximum exceptions before marking as failed
    public int $maxExceptions = 3;

    public function retryUntil(): \DateTime
    {
        // Keep retrying for up to 24 hours
        return now()->addHours(24);
    }

    public function handle(): void
    {
        $response = Http::timeout(30)
            ->retry(2, 1000)
            ->get('https://api.vendor.com/data');

        if ($response->failed()) {
            // Release back to queue with delay for transient failures
            $this->release(60);
            return;
        }

        DataSync::process($response->json());
    }

    public function failed(\Throwable $e): void
    {
        Notification::route('slack', config('services.slack.ops_channel'))
            ->notify(new SyncFailed($e));
    }
}

Rozróżnienie między $tries, $maxExceptions i retryUntil() ma znaczenie podczas rozmów rekrutacyjnych. $tries liczy każdą próbę, w tym ręczne wywołania release. $maxExceptions zlicza wyłącznie nieobsłużone wyjątki. retryUntil() ustawia okno czasowe niezależne od liczby prób.

Zarządzanie workerami i deployment

Workery kolejek na produkcji wymagają nadzoru procesów, łagodnego restartu podczas deploymentu i zarządzania zasobami. Supervisor jest standardowym narzędziem utrzymującym workery przy życiu.

ini
; /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopwaitsecs=3600
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/log/worker.log
stopasgroup=true
killasgroup=true

Kluczowe aspekty deploymentu:

  • Łagodny restart: php artisan queue:restart sygnalizuje workerom zakończenie aktualnego zadania przed restartem. Zapobiega to korupcji zadań podczas deployu.
  • Maks. czas/zadań: --max-time=3600 i --max-jobs=1000 zapobiegają wyciekom pamięci poprzez okresowe recyklowanie procesów workera.
  • Interwał uśpienia: --sleep=3 kontroluje czas oczekiwania workera przed kolejnym odpytaniem pustej kolejki. Niższe wartości zwiększają responsywność, ale również obciążają bazę/Redis.
  • Wiele kolejek: --queue=critical,default,low przetwarza kolejki w kolejności priorytetowej. Workery opróżniają kolejkę critical zanim sięgną po default.

Laravel 12.37 wprowadził połączenie kolejki background, które odracza zadania używając Concurrently::defer(). Ten driver serializuje i uruchamia zadanie w osobnym procesie PHP — przydatne dla lekkich zadań, które nie uzasadniają pełnej infrastruktury kolejkowej.

Unikalne zadania i szyfrowane payloady

Dwa wzorce regularnie pojawiają się w produkcji oraz na rozmowach rekrutacyjnych: zapewnienie, że zadanie wykona się tylko raz dla danego klucza, oraz ochrona wrażliwych danych w payloadach zadań.

App/Jobs/RebuildSearchIndex.phpphp
use Illuminate\Contracts\Queue\ShouldBeUnique;

class RebuildSearchIndex implements ShouldQueue, ShouldBeUnique
{
    // Lock duration in seconds
    public int $uniqueFor = 3600;

    public function __construct(
        public readonly string $indexName
    ) {}

    // Unique key scopes the lock to this specific index
    public function uniqueId(): string
    {
        return $this->indexName;
    }

    public function handle(): void
    {
        SearchIndex::rebuild($this->indexName);
    }
}

Dla zadań zawierających wrażliwe dane (dane logowania, tokeny płatności) interfejs ShouldBeEncrypted szyfruje cały serializowany payload w stanie spoczynku:

App/Jobs/ProcessPayment.phpphp
use Illuminate\Contracts\Queue\ShouldBeEncrypted;

class ProcessPayment implements ShouldQueue, ShouldBeEncrypted
{
    public function __construct(
        private string $paymentToken,
        private float $amount
    ) {}

    public function handle(PaymentGateway $gateway): void
    {
        $gateway->charge($this->paymentToken, $this->amount);
    }
}

Payload jest szyfrowany kluczem aplikacji przed zapisem w Redis lub bazie danych. Workery automatycznie deszyfrują go przed deserializacją.

Najczęstsze pytania rekrutacyjne dotyczące kolejek Laravel

Rozmowy techniczne często sprawdzają zrozumienie architektury kolejek wykraczające poza znajomość API.

Co się dzieje, gdy zakolejkowane zadanie odwołuje się do usuniętego modelu Eloquent? Z trait SerializesModels worker próbuje pobrać model po ID podczas przetwarzania zadania. Jeśli model już nie istnieje, Laravel rzuca ModelNotFoundException. Aby obsłużyć to płynnie, należy ustawić właściwość $deleteWhenMissingModels na true — zadanie zostanie po cichu usunięte z kolejki zamiast zakończyć się błędem.

Czym ShouldBeUnique różni się od middleware WithoutOverlapping? ShouldBeUnique zapobiega dispatchowaniu zadania, jeśli inne z tym samym kluczem unikalnym już istnieje w kolejce. WithoutOverlapping pozwala na dispatch, ale uniemożliwia równoczesne wykonanie — jeśli zadanie z tym samym kluczem już działa, nowa instancja zostaje zwrócona do kolejki. Rozwiązują różne problemy i mogą być łączone.

Kiedy retryUntil() powinno być preferowane nad $tries? retryUntil() sprawdza się dla zadań, które komunikują się z usługami zewnętrznymi, gdzie czas odzyskania jest nieprzewidywalny. Stała liczba ponowień ($tries = 3) może wyczerpać próby podczas krótkiej awarii. retryUntil() ustawia okno czasowe (np. 24 godziny) i kontynuuje ponawianie z backoffem, dopóki usługa nie wróci lub okno nie wygaśnie.

Jak działają priorytety kolejek z wieloma kolejkami? Uruchomienie queue:work --queue=critical,default,low tworzy system priorytetów. Worker w pełni opróżnia kolejkę critical zanim sprawdzi default, a default zanim low. Oznacza to, że zadania o niskim priorytecie mogą głodować w okresach szczytu. Dla rygorystycznych SLA dedykowane workery per kolejka dają lepsze gwarancje.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Podsumowanie

  • Kolejki Laravel abstrakują backend kolejki za niezależnym od sterownika API obsługującym Redis, SQS, bazę danych oraz nowe połączenie background w Laravel 12.37
  • Batching zadań obsługuje obciążenia równoległe ze śledzeniem zbiorczego postępu, podczas gdy łańcuchowanie wymusza sekwencyjne wykonanie dla przepływów domenowych
  • Middleware kolejek (rate limiting, WithoutOverlapping) trzyma zagadnienia przekrojowe poza logiką biznesową zadań
  • Obsługa nieudanych zadań łączy $tries, $maxExceptions, retryUntil() oraz wykładniczy backoff dla odpornych strategii ponawiania
  • ShouldBeUnique zapobiega duplikacji dispatchu; ShouldBeEncrypted chroni wrażliwe payloady w stanie spoczynku
  • Produkcyjne workery wymagają Supervisora, łagodnych restartów podczas deployów oraz zarządzania pamięcią poprzez --max-time i --max-jobs
  • Przygotowanie do rekrutacji powinno obejmować rozróżnienie między unikalnością na etapie dispatchu i lockowaniem na etapie wykonania, zachowanie serializacji modeli oraz głodzenie kolejek priorytetowych

Aby ćwiczyć pytania rekrutacyjne Laravel, bank pytań SharpSkill obejmuje kolejki, middleware oraz wzorce Eloquent ze szczegółowymi wyjaśnieniami.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#laravel
#queues
#jobs
#php
#async
#architecture

Udostępnij

Powiązane artykuły