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

Thêm cron entry một lần duy nhất này vào server của bạn:

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

Điều này chạy mỗi phút và Laravel đánh giá task nào nên thực thi dựa trên schedule của chú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

Kiểm tra cron đã được cấu hình:

crontab -l

Test thủ công:

php artisan schedule:run

Kiểm tra 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