Queue giúp request Laravel trả về nhanh hơn, nhưng đưa công việc sang nền không tự động làm hệ thống đáng tin cậy. Trong production, worker có thể dừng giữa chừng, API bên ngoài có thể timeout và cùng một job có thể được xử lý lại. Một thiết kế tốt phải chủ động kiểm soát retry, timeout, tính idempotent, tài nguyên và khả năng quan sát.
Khi nào nên đưa công việc vào queue?
Queue phù hợp với tác vụ không cần hoàn tất trước khi trả response: gửi email, tạo báo cáo, xử lý ảnh, đồng bộ dữ liệu, phát webhook hoặc nhập file lớn. Những thao tác quyết định trực tiếp kết quả mà người dùng đang chờ, chẳng hạn xác thực quyền hay giữ chỗ tồn kho, thường vẫn cần xử lý đồng bộ hoặc một thiết kế nghiệp vụ riêng.
Queue là ranh giới xử lý bất đồng bộ, không phải nơi giấu mọi đoạn mã chậm.
Chọn Redis và cài Laravel Horizon
Horizon cung cấp dashboard và cấu hình worker cho Laravel Queue dùng Redis. Tài liệu Laravel hiện tại yêu cầu Horizon sử dụng kết nối Redis và lưu ý Horizon chưa tương thích với Redis Cluster. Cài đặt bằng các lệnh sau:
composer require laravel/horizon
php artisan horizon:install
php artisan migrate
Đặt queue connection trong môi trường production:
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
Redis không nên được mở trực tiếp ra Internet. Hãy giới hạn mạng, bật xác thực khi phù hợp, theo dõi bộ nhớ và thiết lập chính sách persistence dựa trên mức độ chấp nhận mất job của hệ thống.
Tạo job có giới hạn rõ ràng
Một job production cần giới hạn số lần thử, thời gian chạy và khoảng nghỉ giữa các lần retry. Mảng backoff giúp tăng dần thời gian chờ khi dịch vụ phụ thuộc đang lỗi tạm thời.
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Throwable;
class SyncInvoice implements ShouldQueue
{
use Queueable;
public int $tries = 5;
public int $timeout = 90;
public bool $failOnTimeout = true;
public function __construct(public int $invoiceId) {}
public function backoff(): array
{
return [10, 30, 120, 300];
}
public function handle(): void
{
// Gọi service với connect timeout và request timeout riêng.
}
public function failed(?Throwable $exception): void
{
// Ghi nhận ngữ cảnh và phát cảnh báo, không nuốt lỗi.
}
}
Chỉ retry lỗi tạm thời như timeout, 429 hoặc 5xx. Lỗi validation, thiếu dữ liệu bắt buộc hay lỗi quyền truy cập thường không tự biến mất sau lần thử tiếp theo; hãy cho job fail có chủ đích và đưa vào luồng xử lý phù hợp.
Ba mốc thời gian phải khớp nhau
Một cấu hình an toàn thường tuân theo thứ tự:
job timeout < Horizon supervisor timeout < Redis retry_after
Ví dụ job timeout 90 giây, Horizon timeout 100 giây và retry_after 120 giây. Horizon cần có cơ hội dừng worker bị treo trước khi queue coi job là mất và giao lại. Nếu timeout của worker dài hơn hoặc quá sát retry_after, hai worker có thể cùng xử lý một job.
Ở tầng trình quản lý tiến trình, stopwaitsecs phải lớn hơn thời gian chạy dài nhất của job để deploy hoặc restart không giết tiến trình giữa công việc hợp lệ.
Dispatch sau khi transaction đã commit
Nếu job phụ thuộc vào dữ liệu vừa ghi trong transaction, worker nhanh có thể chạy trước khi transaction commit và không tìm thấy bản ghi. Có thể bật after_commit cho queue connection hoặc chỉ định trên lần dispatch:
DB::transaction(function () use ($order) {
$order->markAsPaid();
GenerateInvoice::dispatch($order->id)->afterCommit();
});
Chỉ truyền ID và dữ liệu tối thiểu vào job. Serialize một object Eloquent cùng nhiều relationship đã load làm payload lớn, dễ giữ dữ liệu cũ và tăng áp lực lên Redis.
Idempotency: phòng tuyến chống xử lý trùng
Queue thường mang ngữ nghĩa giao ít nhất một lần. Job có thể hoàn tất tác động bên ngoài nhưng worker dừng trước khi xác nhận, khiến job được giao lại. Vì vậy thao tác như trừ tiền, tạo hóa đơn hoặc gửi webhook phải chịu được việc gọi lặp.
- Dùng khóa nghiệp vụ duy nhất, ví dụ
invoice_id + action. - Tạo unique constraint trong database thay vì chỉ kiểm tra bằng mã ứng dụng.
- Lưu trạng thái và cập nhật nghiệp vụ trong cùng transaction khi có thể.
- Dùng transactional outbox nếu cần phối hợp database với message hoặc API bên ngoài.
- Có thể dùng
ShouldBeUniqueđể hạn chế enqueue trùng, nhưng không coi đó là bảo đảm “exactly once”.
Chia queue theo độ ưu tiên và tài nguyên
Không nên để email, ảnh nặng và tác vụ thanh toán cạnh tranh trong một queue. Một cách chia dễ vận hành là high, default, notifications và media. Trong config/horizon.php, tạo supervisor riêng cho workload cần tài nguyên hoặc độ ưu tiên khác nhau.
'production' => [
'supervisor-critical' => [
'connection' => 'redis',
'queue' => ['high'],
'balance' => 'simple',
'processes' => 4,
'tries' => 3,
'timeout' => 100,
],
'supervisor-default' => [
'connection' => 'redis',
'queue' => ['default', 'notifications'],
'balance' => 'auto',
'minProcesses' => 2,
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
'tries' => 5,
'timeout' => 100,
'backoff' => [10, 30, 120],
],
],
auto phân bổ worker theo tải, nhưng không bảo đảm ưu tiên tuyệt đối giữa các queue. Workload quan trọng nên có supervisor riêng và giới hạn worker dựa trên CPU, RAM, kết nối database và rate limit của dịch vụ phụ thuộc.
Chạy Horizon như một dịch vụ
Production cần Supervisor, systemd hoặc trình quản lý tiến trình tương đương để tự khởi động lại Horizon khi máy reboot hoặc tiến trình lỗi. Tiến trình chính là:
php artisan horizon
Dashboard Horizon chứa thông tin vận hành, vì vậy phải giới hạn quyền truy cập qua gate trong HorizonServiceProvider, đồng thời đặt sau HTTPS và cơ chế xác thực của ứng dụng.
Deploy mà không để worker chạy mã cũ
Worker là tiến trình sống lâu và không tự nạp lại source code sau mỗi request. Với Laravel 13, có thể dùng php artisan reload trong quy trình deploy để reload các dịch vụ dài hạn. Riêng Horizon, php artisan horizon:terminate yêu cầu tiến trình hiện tại thoát nhẹ nhàng sau khi hoàn tất job; trình quản lý tiến trình sẽ khởi động phiên bản mới.
php artisan migrate --force
php artisan optimize
php artisan horizon:terminate
Không deploy thay đổi không tương thích với payload job đang chờ. Khi đổi constructor hoặc định dạng dữ liệu, hãy hỗ trợ payload cũ trong giai đoạn chuyển tiếp hoặc làm cạn queue trước khi phát hành.
Xử lý failed jobs và quan sát hệ thống
Đội vận hành cần biết job thất bại vì đâu trước khi retry hàng loạt:
php artisan queue:failed
php artisan queue:retry <job-id>
php artisan queue:retry all
php artisan horizon:forget <job-id>
Dashboard và cảnh báo nên theo dõi độ trễ queue, tuổi của job lâu nhất, throughput, runtime p95/p99, số lần retry, failed jobs, worker restart, bộ nhớ Redis và lỗi từ dịch vụ phụ thuộc. Thiết lập snapshot Horizon theo scheduler và prune dữ liệu theo thời gian lưu giữ phù hợp.
Kiểm thử trước khi đưa lên production
- Dùng
Queue::fake()để xác minh job được dispatch đúng queue và đúng dữ liệu. - Kiểm thử
handle()riêng với lỗi tạm thời và lỗi vĩnh viễn. - Chạy integration test với Redis và worker thật cho đường đi quan trọng.
- Mô phỏng worker dừng sau tác động nghiệp vụ nhưng trước lúc ACK.
- Kiểm tra nhiều job cùng chạy trên một khóa nghiệp vụ.
- Thử deploy khi queue đang có tải và xác minh worker mới dùng đúng phiên bản mã.
Checklist production
- Redis chỉ truy cập từ mạng tin cậy và có giám sát bộ nhớ.
- Mọi job có timeout, số lần thử và backoff hữu hạn.
- Quan hệ timeout tuân theo
job < Horizon < retry_after. - Job phụ thuộc transaction chỉ dispatch sau commit.
- Tác động nghiệp vụ quan trọng có idempotency key và unique constraint.
- Queue nặng hoặc quan trọng có supervisor riêng.
- Horizon chạy dưới process monitor và dashboard được bảo vệ.
- Deploy có bước reload hoặc terminate worker nhẹ nhàng.
- Failed jobs có cảnh báo, người chịu trách nhiệm và quy trình replay.
Kết luận
Laravel Queue đáng tin cậy không nằm ở số lượng worker, mà ở các giới hạn được thiết kế rõ: job có thể retry nhưng không tạo tác động trùng, worker có thể restart mà không mất việc, và đội vận hành nhìn thấy queue đang chậm ở đâu. Redis và Horizon cung cấp nền tảng tốt; tính an toàn đến từ timeout hợp lý, idempotency, chia tải và quy trình deploy có kiểm soát.




Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.