Hàng đợi (Queue) trong Laravel: xử lý tác vụ nặng ở nền

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Gửi email, xuất báo cáo, xử lý ảnh... những việc này mà làm ngay trong request thì người dùng phải chờ rất lâu. Hàng đợi (queue) cho phép bạn đẩy việc nặng ra nền và trả lời người dùng ngay lập tức. Laravel hỗ trợ việc này rất gọn gàng.

Ý tưởng cốt lõi

Thay vì làm việc nặng trong controller, bạn tạo một job mô tả công việc đó và đẩy vào hàng đợi. Một tiến trình riêng gọi là worker sẽ lấy job ra và chạy lần lượt. Request của người dùng kết thúc gần như tức thì.

Laravel hỗ trợ nhiều nơi lưu hàng đợi (driver): database, redis, sqs, beanstalkd. Khi mới học, database là đủ để thử; khi chạy thật, Redis thường là lựa chọn phổ biến nhờ tốc độ.

Tạo và gửi job

Tạo job bằng Artisan:

php artisan make:job SendWelcomeEmail

Rồi viết logic trong phương thức handle:

class SendWelcomeEmail implements ShouldQueue{    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;    public function __construct(public User $user) {}    public function handle(): void    {        Mail::to($this->user->email)->send(new WelcomeMail($this->user));    }}

Đẩy job vào hàng đợi ở bất kỳ đâu trong code:

SendWelcomeEmail::dispatch($user);

Nhờ trait SerializesModels, Laravel chỉ lưu id của model rồi nạp lại bản mới nhất khi job chạy, nên dữ liệu không bị cũ.

Chạy worker

Job chỉ được xử lý khi có worker đang chạy:

php artisan queue:work --tries=3

Trên server thật, bạn không thể để lệnh này chạy trong terminal. Hãy dùng Supervisor (hoặc systemd) để giữ worker luôn sống và tự khởi động lại khi chết. Một lưu ý quan trọng: worker là tiến trình chạy lâu, nó nạp code một lần, nên sau mỗi lần deploy bạn cần chạy php artisan queue:restart để worker nhận code mới.

Xử lý lỗi và thử lại

Job có thể thất bại vì mạng chập chờn hay dịch vụ bên ngoài lỗi. Bạn có thể cấu hình ngay trong class:

public int $tries = 3;public array $backoff = [10, 60, 300]; // giây giữa các lần thử lạipublic function failed(Throwable $e): void{    // ghi log hoặc báo cho đội vận hành}

Job thử hết số lần vẫn lỗi sẽ được ghi vào bảng failed_jobs. Bạn xem bằng php artisan queue:failed và chạy lại bằng php artisan queue:retry all.

Một số kỹ thuật hữu ích

  • Độ trễ: SendReminder::dispatch($user)->delay(now()->addMinutes(10)).
  • Nhiều hàng đợi: onQueue('emails') để tách việc quan trọng khỏi việc ít gấp, rồi chạy worker với --queue=high,default.
  • Chuỗi job: Bus::chain([...])->dispatch() khi các bước phải chạy theo thứ tự.
  • Tính idempotent: job có thể bị chạy lại, nên hãy viết sao cho chạy hai lần không gây hậu quả, ví dụ không trừ tiền hai lần.

Giám sát với Horizon

Nếu dùng Redis, Laravel Horizon cung cấp bảng điều khiển để xem số job đang chờ, thời gian xử lý, job lỗi và tự cân bằng số worker. Đây là công cụ đáng dùng khi hệ thống đã có lượng job đáng kể.

Một ví dụ thực tế: xuất báo cáo

Hãy hình dung người dùng bấm nút xuất báo cáo doanh thu của cả năm. Nếu xử lý đồng bộ, trình duyệt sẽ quay vòng tròn cả phút và có thể bị timeout. Với queue, luồng xử lý trở nên thân thiện hơn rất nhiều:

  1. Controller tạo một bản ghi báo cáo với trạng thái đang xử lý rồi dispatch job, trả về ngay thông báo cho người dùng.
  2. Worker nhận job, truy vấn dữ liệu theo từng đợt nhỏ, ghi ra file và cập nhật trạng thái thành hoàn tất.
  3. Giao diện hỏi trạng thái định kỳ, hoặc ứng dụng gửi email kèm liên kết tải khi xong.

Cách làm này tách biệt hoàn toàn thời gian xử lý khỏi trải nghiệm người dùng, và nếu job lỗi, bạn có cơ hội thử lại mà không bắt người dùng làm lại từ đầu.

Những sai lầm thường gặp

  • Quên chạy worker: job nằm im trong hàng đợi mà không ai xử lý, nhìn như ứng dụng bị hỏng.
  • Truyền dữ liệu quá lớn vào job: chỉ nên truyền id hoặc dữ liệu nhỏ, còn lại hãy nạp lại trong handle.
  • Job chạy quá lâu: hãy chia nhỏ thành nhiều job hoặc đặt $timeout hợp lý để một job không chiếm worker mãi.
  • Dispatch trước khi transaction commit: job có thể chạy khi dữ liệu chưa được lưu. Hãy dùng afterCommit() để job chỉ được gửi đi sau khi giao dịch hoàn tất.

Kết luận

Queue giúp ứng dụng nhanh hơn trong mắt người dùng và chịu tải tốt hơn. Hãy nhớ ba điều: job nhỏ và an toàn khi chạy lại, worker phải được giám sát, và deploy xong phải restart worker. Bắt đầu bằng việc chuyển tác vụ gửi email sang queue là một bước thực hành rất hợp lý.

Nếu muốn luyện tập cùng bài tập thực tế, hãy ghé các khóa học tại codebem.com khi bạn sẵn sàng.

author_photo
Nguyễn Đình Cường

0 comment