Queue và Job trong Laravel: Kiến trúc bất đồng bộ và câu hỏi phỏng vấn 2026

Phân tích sâu kiến trúc queue và job trong Laravel. Bao gồm dispatch job, batching, chaining, middleware, xử lý job thất bại và quản lý queue worker với ví dụ Laravel 12.

Sơ đồ kiến trúc bất đồng bộ của queue và job Laravel với các tiến trình worker và pipeline dispatch job

Queue trong Laravel cung cấp một API thống nhất để hoãn các tác vụ tốn thời gian — gửi email, xử lý upload, tạo báo cáo — sang các worker chạy nền. Thay vì buộc người dùng phải đợi, ứng dụng đẩy job vào queue và tiếp tục công việc khác. Cơ chế này nằm ở trung tâm của mọi ứng dụng Laravel có khả năng mở rộng.

Tổng quan kiến trúc Queue

Laravel hỗ trợ nhiều backend queue (Redis, Amazon SQS, database, Beanstalkd) thông qua một API duy nhất, không phụ thuộc driver. Job là các class PHP được serialize, triển khai interface ShouldQueue. Worker lấy job từ queue, deserialize và thực thi method handle(). Các job thất bại được lưu vào bảng failed_jobs chuyên biệt để retry hoặc kiểm tra.

Cơ chế dispatch job của Laravel hoạt động như thế nào

Khi dispatch() được gọi trên một class job, Laravel serialize instance job — bao gồm các thuộc tính public — và đẩy payload đến queue connection đã cấu hình. Payload được serialize chứa tên class đầy đủ, các thuộc tính được serialize, tên queue đích, và metadata như số lần thử cho phép cùng timeout.

Tiến trình worker (php artisan queue:work) chạy như một daemon dài hạn, liên tục poll backend queue để tìm job mới. Khi nhận được payload, worker deserialize job, giải quyết các dependency thông qua service container, và gọi 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 chỉ lưu primary key và tên class của model, không lưu toàn bộ model Eloquent. Khi worker xử lý job, nó lấy model mới nhất từ database. Cách này tránh được dữ liệu cũ và giữ kích thước payload nhỏ gọn.

Job batching cho khối lượng công việc song song

Job batching gom nhiều job vào một batch duy nhất, theo dõi tiến độ tập thể và kích hoạt callback khi tất cả job hoàn tất — hoặc khi có job nào thất bại. Pattern này phù hợp với import dữ liệu, gửi thông báo hàng loạt và tạo báo cáo, khi nhiều đơn vị công việc độc lập phải hoàn tất trước khi bước cuối cùng chạy.

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 bổ sung metadata cho payload của batch, bao gồm thời gian chờ trong queue và định danh worker. Method allowFailures() ngăn một job thất bại làm hủy toàn bộ batch — quan trọng với các batch import lớn nơi thành công một phần là chấp nhận được.

Job chaining cho luồng công việc tuần tự

Trong khi batching xử lý các khối lượng công việc song song, chaining đảm bảo thực thi tuần tự. Mỗi job trong chain chỉ chạy sau khi job trước đó thành công. Nếu bất kỳ job nào thất bại, phần còn lại của chain bị bỏ và callback catch được kích hoạt.

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();
}

Chaining vượt trội trong các luồng công việc nghiệp vụ nơi thứ tự bước rất quan trọng — thanh toán phải được xác thực trước khi giữ kho, và nhãn vận chuyển phụ thuộc vào kho đã được xác nhận.

Sẵn sàng chinh phục phỏng vấn Laravel?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Queue Middleware cho các vấn đề xuyên suốt

Queue middleware bao bọc việc thực thi job với logic có thể tái sử dụng: rate limiting, khử trùng lặp, hoặc circuit breaker. Thay vì nhúng các vấn đề này vào trong từng job, middleware giữ cho job tập trung vào logic nghiệp vụ.

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 được áp dụng bằng cách định nghĩa method middleware() trên class job:

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 sử dụng atomic lock để đảm bảo chỉ một instance của job (xác định bằng key) chạy tại một thời điểm. Kết hợp với rate limiting, nó ngăn cả việc xử lý trùng lặp lẫn throttling từ API bên ngoài.

Xử lý job thất bại và chiến lược retry

Hệ thống queue production cần xử lý lỗi vững chắc. Laravel lưu các job thất bại trong bảng failed_jobs cùng payload đầy đủ, exception trace, queue và connection đã sinh ra lỗi. Method failed() trên mỗi class job chạy sau khi tất cả lần thử đã hết.

Cấu hình hành vi retry theo từng job mang lại kiểm soát tinh tế:

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));
    }
}

Sự khác biệt giữa $tries, $maxExceptionsretryUntil() rất quan trọng trong các buổi phỏng vấn. $tries đếm mọi lần thử, kể cả release thủ công. $maxExceptions chỉ đếm các exception chưa được xử lý. retryUntil() đặt một cửa sổ thời gian không phụ thuộc vào số lần thử.

Quản lý Queue Worker và deployment

Queue worker trên production cần giám sát tiến trình, restart êm ái khi deploy, và quản lý tài nguyên. Supervisor là công cụ tiêu chuẩn để giữ worker hoạt động liên tục.

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

Các điểm quan trọng khi deploy:

  • Restart êm ái: php artisan queue:restart ra hiệu cho worker hoàn tất job hiện tại trước khi khởi động lại. Điều này ngăn job bị hỏng trong quá trình deploy.
  • Thời gian/job tối đa: --max-time=3600--max-jobs=1000 ngăn rò rỉ bộ nhớ bằng cách tái chế tiến trình worker theo chu kỳ.
  • Khoảng nghỉ: --sleep=3 kiểm soát thời gian worker đợi trước khi poll lại một queue rỗng. Giá trị thấp tăng độ đáp ứng nhưng cũng tăng tải database/Redis.
  • Nhiều queue: --queue=critical,default,low xử lý queue theo thứ tự ưu tiên. Worker xả hết queue critical trước khi chạm vào default.

Laravel 12.37 giới thiệu queue connection background, hoãn job bằng cách dùng Concurrently::defer(). Driver này serialize và chạy job trong một tiến trình PHP riêng — hữu ích cho các job nhẹ không cần đến hạ tầng queue đầy đủ.

Unique Job và payload mã hoá

Hai pattern xuất hiện thường xuyên trong production và các buổi phỏng vấn: đảm bảo một job chỉ chạy một lần với một key cho trước, và bảo vệ dữ liệu nhạy cảm trong payload của job.

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);
    }
}

Với các job mang dữ liệu nhạy cảm (thông tin xác thực người dùng, token thanh toán), interface ShouldBeEncrypted mã hoá toàn bộ payload đã serialize khi lưu trữ:

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 được mã hoá bằng application key trước khi lưu vào Redis hoặc database. Worker tự động giải mã trước khi deserialize.

Câu hỏi phỏng vấn thường gặp về Queue Laravel

Phỏng vấn kỹ thuật thường kiểm tra hiểu biết kiến trúc queue vượt khỏi kiến thức API bề mặt.

Điều gì xảy ra khi một job trong queue tham chiếu một model Eloquent đã bị xoá? Với SerializesModels, worker cố gắng lấy model theo ID khi xử lý job. Nếu model không còn tồn tại, Laravel ném ModelNotFoundException. Để xử lý duyên dáng, đặt thuộc tính $deleteWhenMissingModels thành true — job sẽ tự xoá khỏi queue thay vì thất bại.

ShouldBeUnique khác middleware WithoutOverlapping ở đâu? ShouldBeUnique ngăn job được dispatch nếu đã có job khác cùng unique key trong queue. WithoutOverlapping cho phép dispatch nhưng ngăn thực thi đồng thời — nếu một job với cùng key đang chạy, instance mới được trả lại queue. Chúng giải quyết hai vấn đề khác nhau và có thể kết hợp.

Khi nào nên dùng retryUntil() thay cho $tries? retryUntil() phù hợp với job tương tác với dịch vụ bên ngoài, nơi thời gian phục hồi không thể đoán trước. Số lần retry cố định ($tries = 3) có thể cạn kiệt trong một sự cố ngắn. retryUntil() đặt một cửa sổ thời gian (ví dụ 24 giờ) và tiếp tục retry với backoff cho đến khi dịch vụ phục hồi hoặc cửa sổ hết hạn.

Queue priority hoạt động thế nào với nhiều queue? Chạy queue:work --queue=critical,default,low tạo ra hệ thống ưu tiên. Worker xả hết queue critical trước khi kiểm tra default, và default trước low. Điều này có nghĩa các job ưu tiên thấp có thể bị bỏ đói khi tải đỉnh. Với SLA nghiêm ngặt, worker chuyên dụng theo từng queue mang lại đảm bảo tốt hơn.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Kết luận

  • Queue của Laravel trừu tượng hoá backend queue phía sau một API không phụ thuộc driver, hỗ trợ Redis, SQS, database và connection background mới trong Laravel 12.37
  • Job batching xử lý khối lượng công việc song song với theo dõi tiến độ tập thể, trong khi chaining đảm bảo thực thi tuần tự cho luồng công việc nghiệp vụ
  • Queue middleware (rate limiting, WithoutOverlapping) giữ các vấn đề xuyên suốt nằm ngoài logic nghiệp vụ của job
  • Xử lý job thất bại kết hợp $tries, $maxExceptions, retryUntil() và exponential backoff cho chiến lược retry vững vàng
  • ShouldBeUnique ngăn dispatch trùng lặp; ShouldBeEncrypted bảo vệ payload nhạy cảm khi lưu trữ
  • Worker production cần Supervisor, restart êm ái khi deploy và quản lý bộ nhớ qua --max-time--max-jobs
  • Chuẩn bị phỏng vấn cần bao quát sự khác biệt giữa unique tại thời điểm dispatch và lock tại thời điểm thực thi, hành vi serialize model, và hiện tượng đói queue ưu tiên

Để luyện tập với câu hỏi phỏng vấn Laravel, ngân hàng câu hỏi của SharpSkill bao quát queue, middleware và các pattern Eloquent với giải thích chi tiết.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

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

Chia sẻ

Bài viết liên quan