Toidicode.com

Toidicode.com

BASIC TO ADVANCE

Bài 29: Task Scheduling và Cache trong Laravel 13

Ở bài trước, job nhắc việc vẫn phải dispatch bằng tay, còn trang /stats chạy lại query thống kê mỗi lần mở. 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ề task scheduling và cache trong Laravel 13 để giải quyết cả hai việc đó.

1. Task scheduling là gì?

Thay vì mỗi việc định kỳ (gửi báo cáo, dọn dữ liệu cũ...) là một dòng cron trên server, task scheduling cho phép khai báo lịch chạy ngay trong code, ở routes/console.php. Server chỉ cần một dòng cron gọi php artisan schedule:run mỗi phút, lệnh này chạy các task đã đến giờ.

2. Khai báo lịch chạy.

Các bạn dùng facade Schedule với cú pháp như sau:

use Illuminate\Support\Facades\Schedule;

Schedule::command('ten:lenh --option')->daily(); // Artisan command
Schedule::job(new TenJob)->everyFiveMinutes(); // đưa job vào queue (Bài 28)
Schedule::call(fn () => Log::info('Đã chạy'))->hourly(); // closure

Tần suất hay dùng: everyMinute(), everyFiveMinutes(), hourly(), daily() (0 giờ), dailyAt('08:00'), weeklyOn(1, '8:00') (thứ Hai), monthly(), cron('*/15 * * * *'); ghép thêm điều kiện như weekdays(), between('8:00', '17:00'), timezone('Asia/Ho_Chi_Minh'). Ngoài ra:

  • withoutOverlapping(): lần trước chưa xong thì bỏ qua lần này (lock trong cache, hết hạn sau 24 giờ).
  • onOneServer(): nhiều server thì chỉ một server chạy, cần cache database, memcached, dynamodb hoặc redis dùng chung; call() phải có ->name('...').

3. Nhắc việc hằng ngày cho TaskFlow.

Mỗi sáng 8 giờ, TaskFlow nhắc người được giao các task chưa xong có hạn vào ngày mai. Mình viết nó thành một Artisan command dạng closure (chạy tay được) rồi lên lịch:

- routes/console.php (trích: các dòng use thêm vào đầu file, phần còn lại vào cuối file)

use App\Enums\TaskStatus;
use App\Jobs\SendTaskReminder;
use App\Models\Task;
use Illuminate\Support\Facades\Schedule;

// task chưa xong, đã có người được giao, hết hạn vào ngày mai
Artisan::command('tasks:remind', function () {
    $tasks = Task::query()
        ->whereDate('due_date', today()->addDay())
        ->where('status', '!=', TaskStatus::Done)
        ->whereNotNull('assignee_id')
        ->get();

    $tasks->each(fn (Task $task) => SendTaskReminder::dispatch($task));

    $this->info('Đã đưa '.$tasks->count().' job nhắc việc vào queue.');
})->purpose('Nhắc việc các task hết hạn vào ngày mai');

Schedule::command('tasks:remind')
    ->dailyAt('08:00')
    ->timezone('Asia/Ho_Chi_Minh')
    ->withoutOverlapping()
    ->onOneServer();

$this trong closure là command nên gọi được $this->info(), purpose() là mô tả trong php artisan list. Job vào queue reminders nhờ Queue::route() của Bài 28. config/app.php để múi giờ UTC nên mình chỉ rõ 8 giờ theo giờ Việt Nam:

php artisan schedule:list
#   0 1 * * *  php artisan tasks:remind ................... Next Due: 22 giờ tới

Dữ liệu mẫu chưa có task hạn ngày mai, mình chạy php artisan migrate:fresh --seed rồi đổi hạn hai task trong tinker (task 3 chưa giao cho ai):

Task::find(1)->update(['due_date' => today()->addDay()]);
Task::find(3)->update(['due_date' => today()->addDay()]);

Chạy tay command rồi chạy worker để gửi mail:

php artisan tasks:remind
# Đã đưa 1 job nhắc việc vào queue.

php artisan queue:work --queue=reminders,default
#   2026-11-15 02:20:55 App\Jobs\SendTaskReminder ...................... RUNNING
#   2026-11-15 02:20:55 App\Jobs\SendTaskReminder ................. 60.26ms DONE

laravel.log có mail nhắc task 1 gửi [email protected]. Chưa đến giờ thì php artisan schedule:run chỉ in No scheduled commands are ready to run.; còn schedule:test chạy thử task ngay. Ở local các bạn để php artisan schedule:work chạy trong một terminal riêng (gọi schedule:run mỗi phút), trên server thì thêm một dòng cron (Bài 32):

* * * * * cd /duong-dan-toi-project && php artisan schedule:run >> /dev/null 2>&1

4. Cache trong Laravel.

Cache là nơi lưu tạm kết quả của một việc tốn thời gian (query nặng, gọi API...) để lần sau lấy ra dùng luôn.

Cấu hình ở config/cache.php, mỗi store dùng một driver: database, file, redis, memcached, dynamodb, storage (lưu trên disk, Bài 20), array (dùng khi test). Mặc định CACHE_STORE=database, bảng cache có sẵn (Bài 12). Cần nhanh hơn thì cài extension PhpRedis (hoặc predis/predis) rồi đặt CACHE_STORE=redis. Mọi key được thêm prefix CACHE_PREFIX, mặc định là taskflow-cache-.

Chú ý: serializable_classes trong config/cache.php mặc định là false (Bài 1): object lấy từ cache ra (kể cả Collection, model) thành __PHP_Incomplete_Class. Nên chỉ lưu số, chuỗi, mảng.

5. Làm việc với cache.

Các bạn dùng facade Cache (hoặc helper cache()), VD trong tinker:

Cache::put('site', 'toidicode.com', 600); // lưu 600 giây (nhận cả now()->addMinutes(10))
Cache::get('site'); // "toidicode.com"
Cache::get('khong-co', 'mặc định'); // "mặc định"
Cache::has('site'); // true
Cache::touch('site', 3600); // true, hết hạn sau 3600 giây tính từ bây giờ
Cache::forever('version', 13); // không hết hạn
Cache::pull('version'); // 13, lấy ra rồi xóa
Cache::forget('site'); // true
Cache::remember('so-project', 600, fn () => Project::count()); // 3
Cache::flexible('so-task', [300, 600], fn () => Task::count()); // 10

remember() có trong cache thì trả về luôn, chưa có thì chạy callback và lưu kết quả. flexible() giống vậy trong 300 giây đầu, từ 300 tới 600 giây thì trả ngay giá trị cũ rồi tính lại sau khi gửi response. touch() mới có từ Laravel 13 (Bài 1).

Cache tag (Cache::tags('projects')->flush() xóa cả nhóm key) không có với driver database, file, dynamodb, storage (lỗi BadMethodCallException). Atomic lock là khóa để chỉ một tiến trình làm một việc tại một thời điểm, withoutOverlapping() dùng chính nó:

$lock = Cache::lock('bao-cao', 10); // tự hết hạn sau 10 giây
$lock->get(); // true
Cache::lock('bao-cao', 10)->get(); // false, đang có người giữ
$lock->release(); // true

Xóa toàn bộ cache bằng php artisan cache:clear (lệnh này không xóa route, config cache; xóa hết thì dùng optimize:clear).

6. Cache thống kê task.

Mình cache kết quả TaskStatsService (Bài 25) 10 phút, mỗi user một key. Query cũ chuyển sang method query(int $userId) ($user->id thành $userId):

- app/Services/TaskStatsService.php (trích, thêm use Illuminate\Support\Facades\Cache;)

    // số task theo trạng thái của từng project thuộc $user, cache 10 phút
    public function forUser(User $user): Collection
    {
        // chỉ lưu mảng vào cache, lấy ra thì đổi lại thành object cho view
        $rows = Cache::remember('task-stats:'.$user->id, now()->addMinutes(10), function () use ($user) {
            return $this->query($user->id)->map(fn (object $row) => (array) $row)->all();
        });

        return collect($rows)->map(fn (array $row) => (object) $row);
    }

    // dữ liệu đổi thì xóa cache, lần gọi forUser() sau sẽ query lại
    public function forget(?int $userId): void
    {
        Cache::forget('task-stats:'.$userId);
    }

Dữ liệu đổi thì phải xóa cache. Task thay đổi thì TaskObserver (Bài 26) xóa cache của chủ project:

- app/Observers/TaskObserver.php (trích, thêm use App\Services\TaskStatsService;)

    public function __construct(
        protected TaskStatsService $stats,
    ) {}

    // tạo/sửa (saved) hoặc xóa (deleted) task
    public function saved(Task $task): void
    {
        $this->forgetStats($task);
    }

    public function deleted(Task $task): void
    {
        $this->forgetStats($task);
    }

    // value() chỉ lấy cột user_id, không lazy load project
    protected function forgetStats(Task $task): void
    {
        $this->stats->forget($task->project()->value('user_id'));
    }

Thêm, sửa, xóa project cũng làm thống kê đổi:

- app/Models/Project.php (trích, cuối booted(), thêm use App\Services\TaskStatsService;)

        static::saved(fn (Project $project) => app(TaskStatsService::class)->forget($project->user_id));
        static::deleted(fn (Project $project) => app(TaskStatsService::class)->forget($project->user_id));

Chạy lại php artisan migrate:fresh --seed rồi thử trong tinker:

$stats = app(App\Services\TaskStatsService::class);
$stats->forUser(User::find(1))->pluck('done', 'name'); // ["Website toidicode.com" => 1, "TaskFlow API" => 1]
Cache::has('task-stats:1'); // true
Task::find(3)->update(['status' => TaskStatus::Done]);
Cache::has('task-stats:1'); // false
$stats->forUser(User::find(1))->pluck('done', 'name'); // ["Website toidicode.com" => 2, "TaskFlow API" => 1]

Trang /stats giờ chỉ query lần đầu, sửa task hay tạo project thì số liệu cập nhật ngay.

7. Lời kết.

Như vậy chúng ta đã tìm hiểu xong task scheduling và cache trong Laravel 13. Xem thêm tại tài liệu về scheduling và cache. Đến giờ mình vẫn kiểm tra TaskFlow bằng tay, bài sau mình sẽ giới thiệu về testing với Pest trong Laravel 13 để tự động hóa việc nà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!

Vũ Thanh Tài

About author
The best way to learn is to share
Xem tất cả bài đăng

0 Comments

Bài viết chưa có ai bình luận, hãy là người đầu tiên đi bạn!

Bình luận

Captcha