Site logo
Tác giả
  • avatar Nguyễn Đức Xinh
    Name
    Nguyễn Đức Xinh
    Twitter
Ngày xuất bản
Ngày xuất bản

Laravel Task Scheduling & Cron Jobs: Lập lịch tác vụ trong Laravel

Task Scheduling Là Gì?

Task scheduling là quá trình tự động hóa các tác vụ lặp lại để chạy vào thời gian hoặc khoảng thời gian cụ thể mà không cần can thiệp thủ công. Các trường hợp sử dụng phổ biến bao gồm:

  • Gửi email báo cáo theo lịch
  • Dọn dẹp các bản ghi cũ trong database
  • Tạo báo cáo phân tích hàng ngày
  • Backup database
  • Xử lý các tác vụ hàng loạt (batch operations)
  • Đồng bộ dữ liệu với API bên ngoài

Trong quá khứ, các developer thường sử dụng cron (bộ lập lịch job dựa trên thời gian trong hệ thống Unix-like) để schedule các task, đòi hỏi quyền truy cập server trực tiếp và quản lý cron entry thủ công.

Hiểu Về Cron Jobs

Trước khi tìm hiểu về hệ thống scheduling của Laravel, hãy hiểu những kiến thức cơ bản về cron.

Cú Pháp Cron

Một cron expression bao gồm năm trường thời gian:

* * * * *
│ │ │ │ │
│ │ │ │ └─── Ngày trong tuần (0-7, Chủ nhật = 0 hoặc 7)
│ │ │ └───── Tháng (1-12)
│ │ └─────── Ngày trong tháng (1-31)
│ └───────── Giờ (0-23)
└─────────── Phút (0-59)

Các Ví Dụ Cron Phổ Biến

Expression Mô Tả
* * * * * Mỗi phút
0 * * * * Mỗi giờ
0 0 * * * Hàng ngày lúc nửa đêm
0 0 * * 0 Hàng tuần vào Chủ nhật
0 0 1 * * Hàng tháng vào ngày 1
*/5 * * * * Mỗi 5 phút
0 9-17 * * 1-5 Các ngày trong tuần từ 9 AM đến 5 PM

Hạn Chế Của Cron Truyền Thống

  • Task không được lưu trong source control
  • Yêu cầu quyền SSH để quản lý
  • Không có khả năng hiển thị định nghĩa task
  • Khó test ở local
  • Không có xử lý lỗi tích hợp sẵn
  • Cú pháp phức tạp cho logic điều kiện

Giải Pháp Task Scheduling Của Laravel

Scheduler của Laravel cung cấp một API fluent, elegant để quản lý các scheduled task hoàn toàn trong code ứng dụng của bạn. Lợi ích bao gồm:

  • Version Control: Tất cả schedule đều trong codebase
  • Cú Pháp Rõ Ràng: Các method dễ đọc, có thể chain
  • Chỉ Cần Một Cron Entry: Chỉ cần một cron job duy nhất
  • Dễ Test: Test schedule mà không cần truy cập server
  • Xử Lý Lỗi Tích Hợp: Hooks cho success/failure
  • Ngăn Chặn Overlap: Tránh thực thi task đồng thời
  • Distributed Scheduling: Chạy task trên single server trong môi trường multi-server

Bắt Đầu Với Laravel Scheduler

Bước 1: Định Nghĩa Scheduled Tasks

Tất cả scheduled tasks được định nghĩa trong file routes/console.php hoặc trong phương thức schedule của App\Console\Kernel:

routes/console.php

<?php

use Illuminate\Support\Facades\Schedule;
use Illuminate\Support\Facades\DB;

// Schedule một closure
Schedule::call(function () {
    DB::table('recent_users')->delete();
})->daily();

// Schedule một Artisan command
Schedule::command('emails:send')->dailyAt('13:00');

// Schedule một queued job
Schedule::job(new ProcessPodcast)->everyFiveMinutes();

// Schedule một shell command
Schedule::exec('node /home/forge/script.js')->daily();

app/Console/Kernel.php

<?php

class Kernel extends ConsoleKernel
{
  protected function schedule(Schedule $schedule)
  {
      $schedule->command('emails:send')->dailyAt('13:00')->withoutOverlapping();
  }
}

Ngoài ra, bạn có thể định nghĩa schedule trong bootstrap/app.php:

use Illuminate\Console\Scheduling\Schedule;

->withSchedule(function (Schedule $schedule) {
    $schedule->call(new DeleteRecentUsers)->daily();
})

Bước 2: Thêm Single Cron Entry

Đây là bước duy nhất cần thao tác trực tiếp trên server (SSH), và thường là bước gây bối rối nhất với các bạn backend chưa quen DevOps. Làm theo đúng thứ tự dưới đây:

1. SSH vào server nơi ứng dụng Laravel đang chạy:

ssh user@your-server-ip

2. Xác định user nào đang chạy ứng dụng (ví dụ www-data, forge, deploy, hoặc chính user bạn đang SSH vào). Cron entry nên được thêm cho đúng user này để đảm bảo quyền đọc/ghi file, quyền kết nối database giống hệt khi ứng dụng chạy bình thường qua web:

whoami

3. Xác định đường dẫn tuyệt đối tới project và tới binary php (khi chạy qua cron, biến môi trường PATH thường bị rút gọn nên không thể chỉ gõ php như trên terminal thông thường):

pwd            # chạy tại thư mục gốc project để lấy full path
which php      # ví dụ trả về /usr/bin/php hoặc /usr/local/bin/php

4. Mở crontab để chỉnh sửa (nếu đây là lần đầu, hệ thống có thể hỏi chọn editor — chọn nano nếu không quen vim):

crontab -e

5. Thêm đúng một dòng sau vào cuối file, thay /path-to-your-projectphp bằng giá trị thật đã xác định ở bước 3:

* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

Nếu which php trả về một đường dẫn cụ thể (ví dụ do server dùng nhiều phiên bản PHP), nên dùng full path để tránh chạy nhầm version:

* * * * * cd /path-to-your-project && /usr/bin/php artisan schedule:run >> /dev/null 2>&1

6. Lưu file và thoát (với nano: Ctrl + O để lưu, Enter để xác nhận, Ctrl + X để thoát).

7. Xác nhận lại cron entry đã được lưu đúng:

crontab -l

Dòng cron này chạy mỗi phút và Laravel tự đánh giá task nào nên thực thi dựa trên schedule đã định nghĩa trong code — bạn không cần thêm cron entry mới mỗi khi thêm scheduled task; chỉ cần sửa code và deploy như bình thường.

Lưu ý: Nếu dùng các platform quản lý server có UI như Laravel Forge, Ploi, hay cPanel, các platform này thường có sẵn mục "Scheduler" hoặc "Cron Jobs" trong giao diện, cho phép thêm dòng cron ở trên mà không cần SSH thủ công.

Bước 3: Xem Các Scheduled Tasks

Liệt kê tất cả scheduled tasks và thời gian chạy tiếp theo:

php artisan schedule:list

Các Tùy Chọn Schedule Frequency

Laravel cung cấp các tùy chọn frequency rộng rãi đáp ứng hầu hết mọi nhu cầu scheduling.

Frequency Dựa Trên Thời Gian

// Mỗi giây (sub-minute scheduling)
Schedule::command('monitor:check')->everySecond();
Schedule::command('monitor:check')->everyFiveSeconds();
Schedule::command('monitor:check')->everyTenSeconds();

// Mỗi phút
Schedule::command('process:orders')->everyMinute();
Schedule::command('process:orders')->everyFiveMinutes();
Schedule::command('process:orders')->everyTenMinutes();

// Mỗi giờ
Schedule::command('report:generate')->hourly();
Schedule::command('report:generate')->hourlyAt(17); // Tại phút 17
Schedule::command('report:generate')->everyTwoHours();
Schedule::command('report:generate')->everyThreeHours();

// Hàng ngày
Schedule::command('backup:run')->daily();
Schedule::command('backup:run')->dailyAt('13:00');
Schedule::command('backup:run')->twiceDaily(1, 13); // 1:00 & 13:00

// Hàng tuần
Schedule::command('report:weekly')->weekly();
Schedule::command('report:weekly')->weeklyOn(1, '8:00'); // Thứ 2 lúc 8 AM

// Hàng tháng
Schedule::command('invoice:generate')->monthly();
Schedule::command('invoice:generate')->monthlyOn(4, '15:00'); // Ngày 4 lúc 3 PM
Schedule::command('invoice:generate')->twiceMonthly(1, 16, '13:00');

// Hàng quý và hàng năm
Schedule::command('report:quarterly')->quarterly();
Schedule::command('report:annual')->yearly();
Schedule::command('report:annual')->yearlyOn(6, 1, '17:00'); // 1 tháng 6

Custom Cron Expressions

Để kiểm soát hoàn toàn, sử dụng cú pháp cron tùy chỉnh:

Schedule::command('emails:send')->cron('0 */6 * * *'); // Mỗi 6 giờ

Ràng Buộc Ngày và Thời Gian

// Các ngày cụ thể trong tuần
Schedule::command('emails:send')
    ->hourly()
    ->days([0, 3]); // Chủ nhật và Thứ tư

// Ngày trong tuần/cuối tuần
Schedule::command('report:generate')
    ->weekdays()
    ->at('9:00');

Schedule::command('maintenance:run')
    ->weekends()
    ->at('3:00');

// Các method cho ngày cụ thể
Schedule::command('sales:report')
    ->mondays()
    ->at('8:00');

// Khoảng thời gian
Schedule::command('process:data')
    ->hourly()
    ->between('8:00', '17:00');

Schedule::command('maintenance:run')
    ->hourly()
    ->unlessBetween('9:00', '18:00');

Kỹ Thuật Scheduling Nâng Cao

Ngăn Chặn Task Overlaps

Ngăn một task chạy nếu instance trước đó vẫn đang thực thi:

Schedule::command('emails:send')
    ->everyMinute()
    ->withoutOverlapping();

// Thời gian expiration tùy chỉnh (tính bằng phút)
Schedule::command('process:large-file')
    ->everyFiveMinutes()
    ->withoutOverlapping(10); // Lock hết hạn sau 10 phút

Xóa các lock bị kẹt:

php artisan schedule:clear-cache

Chạy Tasks Trên Single Server

Trong môi trường multi-server, đảm bảo task chỉ chạy trên một server:

Schedule::command('report:generate')
    ->fridays()
    ->at('17:00')
    ->onOneServer();

Yêu cầu:

  • Cache driver: database, memcached, dynamodb, hoặc redis
  • Tất cả server phải chia sẻ cùng một cache server

Đặt tên cho single-server jobs:

Schedule::job(new CheckUptime('https://laravel.com'))
    ->name('check_uptime:laravel.com')
    ->everyFiveMinutes()
    ->onOneServer();

Schedule::job(new CheckUptime('https://vapor.laravel.com'))
    ->name('check_uptime:vapor.laravel.com')
    ->everyFiveMinutes()
    ->onOneServer();

Thực Thi Task Ở Background

Chạy task ở background để tránh blocking:

Schedule::command('analytics:report')
    ->daily()
    ->runInBackground();

// Kết hợp với sub-minute scheduling
Schedule::command('users:delete')
    ->everyTenSeconds()
    ->runInBackground();

Thực Thi Task Có Điều Kiện

Thực thi task dựa trên điều kiện tùy chỉnh:

// Chỉ chạy khi điều kiện là true
Schedule::command('emails:send')
    ->daily()
    ->when(function () {
        return DB::table('settings')->where('key', 'mail_enabled')->value('value');
    });

// Bỏ qua khi điều kiện là true
Schedule::command('backup:run')
    ->daily()
    ->skip(function () {
        return now()->isWeekend();
    });

// Ràng buộc environment
Schedule::command('report:generate')
    ->daily()
    ->environments(['staging', 'production']);

Quản Lý Timezone

Chỉ định timezone cho scheduled tasks:

Schedule::command('report:generate')
    ->timezone('America/New_York')
    ->at('2:00');

Cấu hình timezone global trong config/app.php:

'timezone' => 'UTC',
'schedule_timezone' => 'America/Chicago',

⚠️ Cảnh báo: Hãy cẩn thận với thay đổi giờ mùa hè, vì task có thể chạy hai lần hoặc không chạy.

Xử Lý Maintenance Mode

Mặc định, task không chạy trong maintenance mode. Ghi đè điều này:

Schedule::command('critical:task')
    ->everyFiveMinutes()
    ->evenInMaintenanceMode();

Schedule Groups

Nhóm các task với cấu hình tương tự để tránh lặp lại:

Schedule::daily()
    ->onOneServer()
    ->timezone('America/New_York')
    ->group(function () {
        Schedule::command('emails:send --force');
        Schedule::command('emails:prune');
        Schedule::command('reports:daily');
    });

Quản Lý Task Output

Ghi Log Output Vào Files

// Ghi đè file
Schedule::command('emails:send')
    ->daily()
    ->sendOutputTo($filePath);

// Append vào file
Schedule::command('emails:send')
    ->daily()
    ->appendOutputTo($filePath);

Email Output

// Email output khi hoàn thành
Schedule::command('report:generate')
    ->daily()
    ->sendOutputTo($filePath)
    ->emailOutputTo('admin@example.com');

// Email chỉ khi thất bại
Schedule::command('report:generate')
    ->daily()
    ->emailOutputOnFailure('admin@example.com');

Lưu ý: Cấu hình Laravel mail services trước khi sử dụng email output.

Task Hooks và Callbacks

Before và After Hooks

Schedule::command('emails:send')
    ->daily()
    ->before(function () {
        Log::info('Starting email job');
        // Chuẩn bị resources
    })
    ->after(function () {
        Log::info('Email job completed');
        // Dọn dẹp resources
    });

Success và Failure Handlers

Schedule::command('import:data')
    ->daily()
    ->onSuccess(function () {
        Log::info('Import succeeded');
        Notification::send(User::admins(), new ImportSuccessful());
    })
    ->onFailure(function () {
        Log::error('Import failed');
        Notification::send(User::admins(), new ImportFailed());
    });

Truy Cập Task Output Trong Hooks

use Illuminate\Support\Stringable;

Schedule::command('report:generate')
    ->daily()
    ->onSuccess(function (Stringable $output) {
        // Xử lý output
        if ($output->contains('Error')) {
            Log::warning('Report generated with warnings', [
                'output' => $output->toString()
            ]);
        }
    });

Webhook Pinging

Thông báo cho các service bên ngoài khi task chạy:

Schedule::command('emails:send')
    ->daily()
    ->pingBefore($url) // Trước khi thực thi
    ->thenPing($url); // Sau khi thực thi

// Conditional pinging
Schedule::command('backup:run')
    ->daily()
    ->pingOnSuccess($successUrl)
    ->pingOnFailure($failureUrl);

// Webhook calls có điều kiện
Schedule::command('sync:data')
    ->daily()
    ->pingBeforeIf($condition, $url)
    ->pingOnSuccessIf($condition, $successUrl);

Sub-Minute Scheduling

Laravel hỗ trợ task chạy thường xuyên hơn một lần mỗi phút:

Schedule::call(function () {
    DB::table('recent_users')->delete();
})->everySecond();

Schedule::command('monitor:check')
    ->everyFiveSeconds();

Best practices:

  • Dispatch queued jobs để xử lý thực tế
  • Sử dụng runInBackground() để tránh blocking
  • Tránh các sub-minute task chạy lâu
use App\Jobs\DeleteRecentUsers;

Schedule::job(new DeleteRecentUsers)->everyTenSeconds();

Schedule::command('users:delete')
    ->everyTenSeconds()
    ->runInBackground();

Ngắt Sub-Minute Tasks

Trong quá trình deploy, ngắt các scheduler đang chạy:

php artisan schedule:interrupt

Thêm lệnh này vào deployment script để ngăn code cũ chạy.

Chạy Scheduler Ở Local

Để phát triển ở local, sử dụng lệnh schedule:work thay vì thiết lập cron:

php artisan schedule:work

Lệnh này chạy ở foreground và invoke scheduler mỗi phút cho đến khi bị terminate.

Scheduling Các Loại Task Khác Nhau

1. Scheduling Artisan Commands

// Theo tên command
Schedule::command('emails:send Taylor --force')->daily();

// Theo tên class với arguments
use App\Console\Commands\SendEmailsCommand;

Schedule::command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();

2. Scheduling Artisan Closure Commands

Artisan::command('delete:recent-users', function () {
    DB::table('recent_users')->delete();
})->purpose('Delete recent users')->daily();

// Với arguments
Artisan::command('emails:send {user} {--force}', function ($user) {
    // Logic gửi email
})->purpose('Send emails to specified user')
  ->schedule(['Taylor', '--force'])
  ->daily();

3. Scheduling Queued Jobs

use App\Jobs\Heartbeat;

Schedule::job(new Heartbeat)->everyFiveMinutes();

// Chỉ định queue và connection
Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();

4. Scheduling Shell Commands

Schedule::exec('node /home/forge/script.js')->daily();

Schedule::exec('python /scripts/backup.py')
    ->daily()
    ->sendOutputTo('/logs/backup.log');

5. Scheduling Invokable Objects

class DeleteRecentUsers
{
    public function __invoke()
    {
        DB::table('recent_users')->delete();
    }
}

Schedule::call(new DeleteRecentUsers)->daily();

Scheduled Events

Laravel dispatch các events trong suốt vòng đời scheduling. Bạn có thể tạo listeners cho:

Event Mô Tả
ScheduledTaskStarting Trước khi task bắt đầu
ScheduledTaskFinished Sau khi task hoàn thành
ScheduledBackgroundTaskFinished Background task hoàn thành
ScheduledTaskSkipped Task bị bỏ qua
ScheduledTaskFailed Task thất bại

Ví dụ listener:

namespace App\Listeners;

use Illuminate\Console\Events\ScheduledTaskFailed;
use Illuminate\Support\Facades\Log;

class LogScheduledTaskFailed
{
    public function handle(ScheduledTaskFailed $event)
    {
        Log::error('Scheduled task failed', [
            'task' => $event->task->command,
            'exception' => $event->exception,
        ]);
    }
}

Xử Lý Các Vấn Đề Thường Gặp

Scheduler Không Chạy

Khi scheduled task không thực thi như mong đợi, vấn đề gần như luôn nằm ở một trong hai chỗ: cron entry chưa được cấu hình đúng trên server, hoặc cron service không chạy. Hãy kiểm tra theo thứ tự sau trước khi debug code Laravel.

1. Kiểm tra cron entry đã tồn tại cho đúng user:

crontab -l

Lệnh này liệt kê cron job của user hiện tại. Nếu ứng dụng chạy dưới user khác (ví dụ www-data, forge, deploy), cần chỉ định rõ user đó:

crontab -l -u www-data

Nếu kết quả rỗng hoặc không thấy dòng php artisan schedule:run, đây chính là nguyên nhân — cron entry chưa từng được thêm, hoặc bị thêm nhầm vào crontab của user khác.

2. Kiểm tra cron service (daemon) có đang chạy không:

# Trên hệ thống dùng systemd (Ubuntu, Debian, CentOS 7+)
systemctl status cron      # Debian/Ubuntu
systemctl status crond     # CentOS/RHEL

# Trên hệ thống cũ hơn dùng service
service cron status

Nếu service ở trạng thái inactive hoặc failed, khởi động lại:

sudo systemctl restart cron
sudo systemctl enable cron   # đảm bảo tự khởi động cùng server

3. Kiểm tra system log để xem cron có thực sự trigger hay không:

# Debian/Ubuntu
grep CRON /var/log/syslog | tail -n 20

# CentOS/RHEL
tail -f /var/log/cron

Nếu thấy dòng log ghi nhận cron đã chạy php artisan schedule:run đúng mỗi phút nhưng task Laravel vẫn không thực thi, vấn đề chuyển sang phía Laravel (đường dẫn project sai, quyền file, PHP CLI khác version với PHP-FPM...) chứ không còn là vấn đề cron nữa.

4. Test thủ công lệnh schedule:run:

php artisan schedule:run

Chạy trực tiếp bằng đúng user và đúng path như trong crontab để loại trừ vấn đề về quyền hoặc biến môi trường (PATH, .env không được load khi chạy qua cron).

5. Kiểm tra danh sách task và thời gian chạy tiếp theo:

php artisan schedule:list

Giúp xác nhận task có được đăng ký đúng và Laravel tính toán đúng thời gian chạy kế tiếp hay chưa.

6. Kiểm tra Laravel logs:

tail -f storage/logs/laravel.log

Tasks Chạy Nhiều Lần

  • Đảm bảo onOneServer() được sử dụng trong thiết lập multi-server
  • Xác minh cấu hình cache cho distributed locks
  • Kiểm tra các cron entry trùng lặp

Overlapping Tasks

// Thêm overlap prevention
Schedule::command('long-running-task')
    ->everyMinute()
    ->withoutOverlapping();

Tasks Không Chạy Đúng Giờ

  • Xác minh timezone của server: date
  • Kiểm tra cấu hình schedule_timezone
  • Sử dụng timezone cụ thể trong định nghĩa task

Xóa Stuck Locks

php artisan schedule:clear-cache

So Sánh: Traditional Cron vs Laravel Scheduler

Tính Năng Traditional Cron Laravel Scheduler
Vị Trí Cấu Hình Server crontab Application code
Version Control ❌ Không ✅ Có
Cú Pháp Cron expressions phức tạp Fluent, readable methods
Testing Khó khăn Dễ dàng với Artisan commands
Conditional Logic Giới hạn Hỗ trợ đầy đủ PHP logic
Error Handling Cần thiết lập thủ công Built-in hooks
Output Management Redirection phức tạp Simple method calls
Multi-server Support Phối hợp thủ công Built-in onOneServer()
Overlap Prevention Manual flock/pidfiles Built-in withoutOverlapping()
Sub-minute Tasks Không hỗ trợ Được hỗ trợ
Deployment Cần quyền truy cập server Deploy với code

Kết Luận

Hệ thống task scheduling của Laravel biến đổi quản lý cron job truyền thống thành một giải pháp elegant, có thể test và dễ bảo trì. Bằng cách thành thạo scheduler, bạn có thể:

  • Tự động hóa các tác vụ lặp lại với cấu hình tối thiểu
  • Duy trì schedules trong version control cùng với application code
  • Triển khai logic scheduling phức tạp với cú pháp fluent, dễ đọc
  • Xử lý distributed systems với single-server execution tích hợp sẵn
  • Giám sát và debug tasks với hooks và events toàn diện
  • Scale một cách tự tin biết rằng overlap prevention và queue integration đã được tích hợp sẵn

Dù bạn đang xây dựng một blog đơn giản cần cleanup hàng ngày hay một ứng dụng SaaS phức tạp với yêu cầu scheduling phức tạp, scheduler của Laravel cung cấp các công cụ bạn cần để tự động hóa hiệu quả và đáng tin cậy.

Hãy bắt đầu đơn giản với các schedule cơ bản, sau đó dần dần triển khai các tính năng nâng cao như single-server execution, overlap prevention, và giám sát toàn diện khi ứng dụng của bạn phát triển. Scheduler sẽ scale theo nhu cầu của bạn trong khi giữ code sạch sẽ và dễ bảo trì.

Tài Nguyên Bổ Sung