Ở bài trước, mail chào mừng và thông báo giao task được gửi ngay trong request, với SMTP thật thì người dùng phải chờ vài giây. Tiếp tục với series, bài này mình sẽ giới thiệu với mọi người về queue và job trong Laravel 13 để đẩy những việc chậm như vậy ra chạy nền.
1. Queue và job là gì?
Job là một việc cần làm (gửi mail, xử lý file...) được đóng gói thành class, queue là hàng đợi chứa các job để một tiến trình khác (worker) lấy ra chạy dần.
Request chỉ việc đưa job vào queue rồi trả response ngay. Cấu hình nằm ở config/queue.php: mỗi connection là một nơi lưu job (database, redis, sqs, sync chạy job ngay không qua queue...), trong một connection có thể có nhiều queue đặt tên tùy ý, mặc định là default.
Project mới đã có QUEUE_CONNECTION=database trong .env, và migration 0001_01_01_000002_create_jobs_table (Bài 12) tạo sẵn ba bảng: jobs (job đang chờ), job_batches (mục 6) và failed_jobs (mục 5).
2. Tạo job.
Mình tạo job SendTaskReminder gửi mail nhắc người được giao một task chưa xong:
php artisan make:job SendTaskReminder
# INFO Job [app/Jobs/SendTaskReminder.php] created successfully.
- app/Jobs/SendTaskReminder.php
<?php
namespace App\Jobs;
use App\Models\Task;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Mail\Message;
use Illuminate\Queue\Attributes\Backoff;
use Illuminate\Queue\Attributes\FailOnTimeout;
use Illuminate\Queue\Attributes\Timeout;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Support\Facades\Mail;
use RuntimeException;
#[Tries(3)]
#[Backoff([10, 30])]
#[Timeout(30)]
#[FailOnTimeout]
class SendTaskReminder implements ShouldQueue
{
use Queueable;
public function __construct(
public Task $task,
) {}
public function handle(): void
{
$assignee = $this->task->assignee;
// ném exception thì job được thử lại, biết đâu lúc đó task đã được giao
if ($assignee === null) {
throw new RuntimeException('Task #'.$this->task->id.' chưa có người được giao.');
}
$due = $this->task->due_date?->format('d/m/Y') ?? 'chưa đặt';
Mail::raw(
'Task "'.$this->task->title.'" của bạn chưa xong, hạn: '.$due.'.',
fn (Message $message) => $message->to($assignee->email)->subject('Nhắc việc: '.$this->task->title),
);
}
}
Trong đó:
ShouldQueue: dispatch thì job được đưa vào queue thay vì chạy ngay. Model truyền vào constructor chỉ được lưu id, lúc chạy lấy lại từ database (giốngSerializesModelsở Bài 26).handle(): code chạy khi worker xử lý job, inject được service qua tham số (Bài 25).Mail::raw()gửi mail chỉ có chữ, không cần Mailable.#[Tries(3)]: thử tối đa 3 lần (mặc định 1).#[Backoff([10, 30])]: chờ 10 giây trước lần thử thứ 2, 30 giây trước lần thứ 3.#[Timeout(30)]: chạy quá 30 giây thì bị dừng (cần extensionpcntl),#[FailOnTimeout]: quá giờ thì thất bại luôn, không thử lại. Các attribute này mình đã nhắc qua ở Bài 1.
3. Dispatch job và chạy worker.
Mình cho job nhắc việc vào queue riêng reminders bằng Queue::route() mới của Laravel 13, khỏi gọi ->onQueue() mỗi lần dispatch:
- app/Providers/AppServiceProvider.php (trích, thêm use App\Jobs\SendTaskReminder;, use Illuminate\Support\Facades\Queue;)
// cuối boot(): job nhắc việc luôn vào queue "reminders" của connection database
Queue::route(SendTaskReminder::class, connection: 'database', queue: 'reminders');
Vừa thêm class mới nên chạy composer dump-autoload (Bài 15), php artisan migrate:fresh --seed, rồi vào php artisan tinker:
SendTaskReminder::dispatch(Task::find(1)); // tham số truyền vào constructor, job vào queue
SendTaskReminder::dispatch(Task::find(6))->delay(now()->addMinutes(10)); // 10 phút nữa mới được chạy
DB::table('jobs')->pluck('queue')->all(); // ["reminders", "reminders"]
SendTaskReminder::dispatchSync(Task::find(9)); // chạy ngay, không qua queue
Lúc này laravel.log mới có mail của task 9, hai job kia nằm trong bảng jobs chờ worker. Mở một terminal khác và chạy:
php artisan queue:work --queue=reminders,default
# INFO Processing jobs from the [reminders,default] queues.
#
# 2026-11-15 02:09:57 App\Jobs\SendTaskReminder ...................... RUNNING
# 2026-11-15 02:09:57 App\Jobs\SendTaskReminder ................. 49.71ms DONE
--queue liệt kê các queue theo thứ tự ưu tiên: hết job ở reminders mới lấy default. Mail của task 1 đã vào log, job của task 6 vẫn chờ đủ 10 phút. Worker chạy cho tới khi bấm Ctrl+C.
queue:work nạp ứng dụng một lần rồi giữ trong bộ nhớ, sửa code xong phải khởi động lại worker (php artisan queue:restart). queue:listen nạp lại code cho mỗi job, chậm hơn nhưng tiện khi đang code. composer run dev (Bài 2) có chạy queue:listen nhưng chỉ cho queue default, nên vẫn cần chạy thêm lệnh trên cho reminders. Trên server, worker được Supervisor giữ cho luôn chạy, phần này ở Bài 32.
4. Đưa notification và mail vào queue.
Notification, Mailable và listener chỉ cần implements ShouldQueue là được đẩy vào queue, code gửi giữ nguyên. Mình làm vậy với TaskAssigned của Bài 27:
- app/Notifications/TaskAssigned.php (trích)
use Illuminate\Contracts\Queue\ShouldQueue;
class TaskAssigned extends Notification implements ShouldQueue
{
use Queueable;
WelcomeMail cũng thêm implements ShouldQueue (cùng use ở trên), nên Mail::to($user)->send() trong RegisterController giờ chỉ đẩy mail vào queue. Không muốn sửa class thì gọi Mail::to($user)->queue(...).
Giao task 7 cho Bình trong tinker (Task::find(7)->update(['assignee_id' => 2]);): bảng notifications chưa có gì, queue default có 2 job, mỗi kênh một job. Worker xử lý chúng:
# 2026-11-15 02:10:02 App\Notifications\TaskAssigned ................. RUNNING
# 2026-11-15 02:10:03 App\Notifications\TaskAssigned ........... 180.04ms DONE
# 2026-11-15 02:10:03 App\Notifications\TaskAssigned ................. RUNNING
# 2026-11-15 02:10:03 App\Notifications\TaskAssigned ............ 16.76ms DONE
Chú ý: từ giờ chạy TaskFlow luôn cần một worker, nếu không mail và thông báo sẽ nằm mãi trong bảng jobs.
5. Thử lại và job thất bại.
Job ném exception thì worker ghi lỗi vào log rồi trả job về queue để thử lại theo #[Tries] và #[Backoff]. VD nhắc việc cho task 3 chưa có ai được giao: SendTaskReminder::dispatch(Task::find(3));
# 2026-11-15 02:10:08 App\Jobs\SendTaskReminder ...................... RUNNING
# 2026-11-15 02:10:08 App\Jobs\SendTaskReminder ................. 21.57ms FAIL
# 2026-11-15 02:10:20 App\Jobs\SendTaskReminder ...................... RUNNING
# 2026-11-15 02:10:20 App\Jobs\SendTaskReminder .................. 8.49ms FAIL
# 2026-11-15 02:10:50 App\Jobs\SendTaskReminder ...................... RUNNING
# 2026-11-15 02:10:50 App\Jobs\SendTaskReminder .................. 9.79ms FAIL
Lần 2 chạy sau khoảng 10 giây, lần 3 sau 30 giây (cộng vài giây worker nghỉ chờ job). Hết 3 lần, job được chuyển sang bảng failed_jobs:
php artisan queue:failed
# 2026-11-15 02:10:50 8ecf4759-33d1-44ad-b5a3-fa6be7f2c37d database@reminders App\Jobs\SendTaskReminder
Giao task 3 cho Bình (Task::find(3)->update(['assignee_id' => 2]);) rồi đưa job thất bại trở lại queue bằng id của nó, hoặc all cho tất cả:
php artisan queue:retry all
# INFO Pushing failed queue jobs back onto the queue.
#
# 8ecf4759-33d1-44ad-b5a3-fa6be7f2c37d .......................... 24.27ms DONE
Worker vẫn đang chạy nên 2 job TaskAssigned sinh ra khi giao task được chạy luôn; sau queue:retry thì job nhắc việc chạy lại và thành công. queue:forget <id> xóa một job thất bại, queue:flush xóa hết. Ngoài ra, gọi $this->fail('lý do') trong handle() thì job thất bại luôn không thử lại, method failed(?Throwable $exception) của job chạy khi job thất bại hẳn.
6. Chain, batch và Horizon.
Bus::chain() chạy các job lần lượt, job sau chỉ chạy khi job trước thành công (trích, trong code của ứng dụng):
use App\Jobs\SendTaskReminder;
use App\Models\Task;
use Illuminate\Support\Facades\Bus;
Bus::chain([
new SendTaskReminder(Task::find(1)),
new SendTaskReminder(Task::find(6)),
])->dispatch();
Bus::batch([...]) thì chạy cả nhóm job (song song nếu có nhiều worker), lưu tiến độ ở bảng job_batches và gọi callback then(), catch(), finally() khi xong; job trong batch cần thêm trait Illuminate\Bus\Batchable. Khi dùng Redis làm queue, Laravel Horizon cho một dashboard theo dõi job và cấu hình worker bằng code.
7. Lời kết.
Như vậy chúng ta đã tìm hiểu xong queue và job trong Laravel 13: tạo job, dispatch, chạy worker, Queue::route(), đưa notification và mail vào queue, thử lại và xử lý job thất bại. Các bạn xem thêm tại tài liệu chính thức. Job nhắc việc hiện vẫn phải dispatch bằng tay, bài sau mình sẽ giới thiệu về task scheduling và cache trong Laravel 13 để nó tự chạy hằng ngày.
Đăng ký nhận tin.
Chúng tôi chỉ gửi tối đa 2 lần trên 1 tháng. Tuyên bố không spam mail!





0 Comments