简介

过去,你可能需要为服务器上需要调度的每个任务编写一条 cron 配置条目。然而,这很快就会变成一件麻烦事,因为你的任务计划不再处于源码控制中,你必须通过 SSH 登录到服务器才能查看现有的 cron 条目或添加新的条目。

Laravel 的命令调度器为管理服务器上的定时任务提供了一种全新的方法。调度器允许你在 Laravel 应用程序内部流畅且富有表现力地定义命令调度。使用调度器时,你的服务器上只需要一条 cron 条目。你的任务调度通常定义在应用程序的 routes/console.php 文件中。

定义调度

你可以在应用程序的 routes/console.php 文件中定义所有定时任务。首先,让我们来看一个示例。在本示例中,我们将调度一个闭包,使其每天午夜执行。在闭包中,我们将执行一条数据库查询来清空一张表:

<?php

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

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

除了使用闭包进行调度外,你还可以使用可调用对象进行调度。可调用对象是包含 __invoke 方法的简单 PHP 类:

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

如果你希望将 routes/console.php 文件仅用于命令定义,你可以在应用程序的 bootstrap/app.php 文件中使用 withSchedule 方法来定义你的定时任务。该方法接收一个闭包,该闭包会接收调度器的一个实例:

use Illuminate\Console\Scheduling\Schedule;

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

如果你想查看定时任务的概览以及它们下一次计划运行的时间,可以使用 schedule:list Artisan 命令:

php artisan schedule:list

调度 Artisan 命令

除了调度闭包,你还可以调度Artisan 命令和系统命令。例如,你可以使用 command 方法,通过命令名称或类来调度 Artisan 命令。

当使用命令的类名来调度 Artisan 命令时,你可以传递一个额外的命令行参数数组,这些参数会在命令被调用时提供给该命令:

use App\Console\Commands\SendEmailsCommand;
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send Taylor --force')->daily();

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

调度 Artisan 闭包命令

如果你想调度由闭包定义的 Artisan 命令,你可以在命令定义之后链式调用与调度相关的方法:

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

如果你需要向闭包命令传递参数,你可以将它们提供给 schedule 方法:

Artisan::command('emails:send {user} {--force}', function ($user) {
    // ...
})->purpose('Send emails to the specified user')->schedule(['Taylor', '--force'])->daily();

调度队列任务

可以使用 job 方法来调度队列任务。该方法提供了一种便捷的方式来调度队列任务,无需使用 call 方法定义用于将任务加入队列的闭包:

use App\Jobs\Heartbeat;
use Illuminate\Support\Facades\Schedule;

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

可以为 job 方法提供可选的第二个和第三个参数,分别指定用于将任务加入队列的队列名称和队列连接:

use App\Jobs\Heartbeat;
use Illuminate\Support\Facades\Schedule;

// Dispatch the job to the "heartbeats" queue on the "sqs" connection...
Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();

调度 Shell 命令

可以使用 exec 方法向操作系统发出命令:

use Illuminate\Support\Facades\Schedule;

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

调度频率选项

我们已经看到了一些示例,展示了如何配置任务以特定间隔运行。然而,还有更多可分配给任务的任务调度频率:

方法描述
->cron('* * * * *');按照自定义 cron 计划运行任务。
->everySecond();每秒运行一次任务。
->everyTwoSeconds();每两秒运行一次任务。
->everyFiveSeconds();每五秒运行一次任务。
->everyTenSeconds();每十秒运行一次任务。
->everyFifteenSeconds();每十五秒运行一次任务。
->everyTwentySeconds();每二十秒运行一次任务。
->everyThirtySeconds();每三十秒运行一次任务。
->everyMinute();每分钟运行一次任务。
->everyTwoMinutes();每两分钟运行一次任务。
->everyThreeMinutes();每三分钟运行一次任务。
->everyFourMinutes();每四分钟运行一次任务。
->everyFiveMinutes();每五分钟运行一次任务。
->everyTenMinutes();每十分钟运行一次任务。
->everyFifteenMinutes();每十五分钟运行一次任务。
->everyThirtyMinutes();每三十分钟运行一次任务。
->hourly();每小时运行一次任务。
->hourlyAt(17);每小时的第 17 分钟运行一次任务。
->everyOddHour($minutes = 0);每个奇数小时运行一次任务。
->everyTwoHours($minutes = 0);每两小时运行一次任务。
->everyThreeHours($minutes = 0);每三小时运行一次任务。
->everyFourHours($minutes = 0);每四小时运行一次任务。
->everySixHours($minutes = 0);每六小时运行一次任务。
->daily();每天午夜运行一次任务。
->dailyAt('13:00');每天 13:00 运行一次任务。
->twiceDaily(1, 13);每天的 1:00 和 13:00 各运行一次任务。
->twiceDailyAt(1, 13, 15);每天的 1:15 和 13:15 各运行一次任务。
->daysOfMonth([1, 10, 20]);在每月的指定日期运行任务。
->weekly();每周日 00:00 运行一次任务。
->weeklyOn(1, '8:00');每周一 8:00 运行一次任务。
->monthly();每月第一天 00:00 运行一次任务。
->monthlyOn(4, '15:00');每月 4 日 15:00 运行一次任务。
->twiceMonthly(1, 16, '13:00');每月 1 日和 16 日的 13:00 各运行一次任务。
->lastDayOfMonth('15:00');每月最后一天的 15:00 运行一次任务。
->quarterly();每季度第一天 00:00 运行一次任务。
->quarterlyOn(4, '14:00');每季度 4 日 14:00 运行一次任务。
->yearly();每年第一天 00:00 运行一次任务。
->yearlyOn(6, 1, '17:00');每年 6 月 1 日 17:00 运行一次任务。
->timezone('America/New_York');为任务设置时区。

这些方法可以与其他约束条件结合使用,创建更精细的调度计划,使其仅在一周的特定日期运行。例如,你可以调度一个命令在每周一运行:

use Illuminate\Support\Facades\Schedule;

// Run once per week on Monday at 1 PM...
Schedule::call(function () {
    // ...
})->weekly()->mondays()->at('13:00');

// Run hourly from 8 AM to 5 PM on weekdays...
Schedule::command('foo')
    ->weekdays()
    ->hourly()
    ->timezone('America/Chicago')
    ->between('8:00', '17:00');

可在下方查看额外的调度约束条件列表:

方法说明
->weekdays();将任务限制在工作日。
->weekends();将任务限制在周末。
->sundays();将任务限制在周日。
->mondays();将任务限制在周一。
->tuesdays();将任务限制在周二。
->wednesdays();将任务限制在周三。
->thursdays();将任务限制在周四。
->fridays();将任务限制在周五。
->saturdays();将任务限制在周六。
->days(array|mixed);将任务限制在特定日期。
->between($startTime, $endTime);将任务限制在开始时间与结束时间之间运行。
->unlessBetween($startTime, $endTime);将任务限制为不在开始时间与结束时间之间运行。
->when(Closure);根据真值测试限制任务。
->environments($env);将任务限制在特定环境。

日期约束

可以使用 days 方法将任务的执行限制在一周内的特定日期。例如,您可以安排一个命令在周日和周三每小时运行:

use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')
    ->hourly()
    ->days([0, 3]);

此外,在定义任务应运行的日期时,可以使用 Illuminate\Console\Scheduling\Schedule 类中提供的常量:

use Illuminate\Support\Facades;
use Illuminate\Console\Scheduling\Schedule;

Facades\Schedule::command('emails:send')
    ->hourly()
    ->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);

时间区间约束

可以使用 between 方法根据一天中的时间限制任务的执行:

Schedule::command('emails:send')
    ->hourly()
    ->between('7:00', '22:00');

类似地,可以使用 unlessBetween 方法排除任务在一段时间内的执行:

Schedule::command('emails:send')
    ->hourly()
    ->unlessBetween('23:00', '4:00');

真值测试约束

可以使用 when 方法根据给定真值测试的结果限制任务的执行。换句话说,如果给定闭包返回 true,只要没有其他限制条件阻止任务运行,任务就会执行:

Schedule::command('emails:send')->daily()->when(function () {
    return true;
});

可以将 skip 方法视为 when 的反向操作。如果 skip 方法返回 true,计划任务将不会执行:

Schedule::command('emails:send')->daily()->skip(function () {
    return true;
});

使用链式 when 方法时,计划命令仅在所有 when 条件都返回 true 时才会执行。

环境约束

可以使用 environments 方法仅在给定环境(由 APP_ENV 环境变量定义)中执行任务:

Schedule::command('emails:send')
    ->daily()
    ->environments(['staging', 'production']);

时区

使用 timezone 方法,您可以指定计划任务的时间应在给定时区内进行解析:

use Illuminate\Support\Facades\Schedule;

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

如果需要为所有计划任务重复指定同一时区,可以在应用程序的 app 配置文件中定义 schedule_timezone 选项,以指定所有调度应使用的时区:

'timezone' => 'UTC',

'schedule_timezone' => 'America/Chicago',

请注意,某些时区会使用夏令时。当发生夏令时切换时,您的计划任务可能会运行两次,甚至完全不运行。因此,我们建议尽可能避免使用时区调度。

防止任务重叠

默认情况下,即使任务的前一个实例仍在运行,计划任务仍会继续运行。若要防止这种情况,可以使用 withoutOverlapping 方法:

use Illuminate\Support\Facades\Schedule;

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

在此示例中,emails:send Artisan 命令如果尚未在运行,则会每分钟运行一次。withoutOverlapping 方法在任务执行时间差异较大时特别有用,可避免您无法准确预测某个任务的执行时长。

如果需要,您可以指定在“无重叠”锁过期之前必须经过多少分钟。默认情况下,锁会在 24 小时后过期:

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

在底层实现中,withoutOverlapping 方法会利用应用程序的缓存来获取锁。如有必要,可以使用 schedule:clear-cache Artisan 命令清除这些缓存锁。通常只有在任务因意外的服务器问题而卡住时,才需要进行此操作。

在单台服务器上运行任务

若要使用此功能,您的应用程序必须将 database、memcached、dynamodb 或 redis 缓存驱动用作应用程序的默认缓存驱动。此外,所有服务器都必须与同一个中央缓存服务器通信。

如果应用程序的调度器运行在多台服务器上,您可以将计划任务限制为仅在单台服务器上执行。例如,假设您有一个计划任务,会在每周五晚上生成一份新报告。如果任务调度器运行在三台工作服务器上,该计划任务会在这三台服务器上均运行,并生成报告三次。这可不行!

若要表明任务应仅在单台服务器上运行,可在定义计划任务时使用 onOneServer 方法。最先获取任务的服务器会对该任务加上原子锁,以防止其他服务器在同一时间运行相同任务:

use Illuminate\Support\Facades\Schedule;

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

若要将 onOneServer 应用于所有计划任务,可以在应用程序的 AppServiceProvider 的 boot 方法中调用 alwaysOnOneServer 方法:

use Illuminate\Support\Facades\Schedule;

public function boot(): void
{
    Schedule::alwaysOnOneServer();
}

未命名的计划闭包会被排除在外,并将继续在每台服务器上运行。

可以使用 useCache 方法来自定义调度器用于获取单服务器任务所需原子锁的缓存存储:

Schedule::useCache('database');

为单服务器任务命名

有时您可能需要调度同一个任务,使其使用不同参数进行分发,同时仍指示 Laravel 在单台服务器上运行该任务的每种排列。为此,可以通过 name 方法为每个调度定义分配一个唯一的名称:

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();

类似地,如果计划闭包要在单台服务器上运行,则必须为其分配名称:

Schedule::call(fn () => User::resetApiRequestCount())
    ->name('reset-api-request-count')
    ->daily()
    ->onOneServer();

后台任务

默认情况下,在同一时间调度的多个任务会根据其在 schedule 方法中的定义顺序依次执行。如果存在长时间运行的任务,这可能会导致后续任务的启动时间远晚于预期。若希望在后台运行任务,使其能够同时执行,可以使用 runInBackground 方法:

use Illuminate\Support\Facades\Schedule;

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

只有在通过 command 和 exec 方法调度任务时,才能使用 runInBackground 方法。

维护模式

当应用处于维护模式时,应用的定时任务不会运行,因为我们不希望任务干扰你在服务器上可能正在进行的尚未完成的维护工作。不过,如果你希望即使在维护模式下也强制运行某个任务,可以在定义任务时调用 evenInMaintenanceMode 方法:

Schedule::command('emails:send')->evenInMaintenanceMode();

暂停定时任务

你可以使用 schedule:pause Artisan 命令,在不修改已部署代码的情况下,暂时暂停定时任务的执行:

php artisan schedule:pause

调度器处于暂停状态时,任何定时任务都不会运行。你可以使用 schedule:continue 命令恢复定时任务的执行:

php artisan schedule:continue

如果希望某个任务在调度器暂停时依然运行,可以使用 evenWhenPaused 方法对该任务进行标记:

Schedule::command('emails:send')->evenWhenPaused();

任务分组

当需要定义多个配置相似的定时任务时,可以利用 Laravel 的任务分组功能,避免在每个任务中重复设置相同配置。对任务进行分组既能简化代码,又能确保相关任务之间的一致性。

要创建一组定时任务,先调用所需的任务配置方法,再调用 group 方法。group 方法接收一个闭包,该闭包负责定义共享指定配置的任务:

use Illuminate\Support\Facades\Schedule;

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

运行调度器

了解如何定义定时任务后,我们来探讨如何在服务器上实际运行这些任务。schedule:run Artisan 命令会对所有定时任务进行评估,并根据服务器当前时间判断是否需要运行。

因此,在使用 Laravel 调度器时,我们只需在服务器的 cron 配置中添加一条条目,使其每分钟运行一次 schedule:run 命令。如果你不清楚如何在服务器中添加 cron 条目,可以考虑使用托管平台(如Laravel Cloud),该平台能够为你管理定时任务的执行:

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

亚分钟级定时任务

在大多数操作系统中,cron 任务的运行频率上限为每分钟一次。然而,Laravel 的调度器支持以更短的时间间隔调度任务,甚至可以做到每秒运行一次:

use Illuminate\Support\Facades\Schedule;

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

当应用中定义了亚分钟级任务时,schedule:run 命令不会立即退出,而是会持续运行至当前分钟结束。这样一来,该命令就能在当前分钟内调用所有需要运行的亚分钟级任务。

由于运行时间超出预期的亚分钟级任务可能会延误后续任务的执行,因此建议所有亚分钟级任务通过派发队列任务或后台命令来处理实际的任务逻辑:

use App\Jobs\DeleteRecentUsers;

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

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

中断亚分钟级任务

由于定义了亚分钟级任务时,schedule:run 命令会在其被调用的整分钟内持续运行,所以在部署应用时,有时需要中断该命令。否则,正在运行的 schedule:run 命令实例会在当前分钟结束前,继续使用应用此前部署的代码。

若要中断正在执行的 schedule:run 调用,可以在应用的部署脚本中添加 schedule:interrupt 命令。该命令应在应用部署完成后执行:

php artisan schedule:interrupt

在本地运行调度器

通常情况下,你无需在本地开发环境中配置调度器的 cron 条目。你可以改用 schedule:work Artisan 命令,该命令会在前台运行,并且每分钟调用一次调度器,直至你终止该命令。当定义了亚分钟级任务时,调度器会在每一分钟内持续运行,以处理这些任务:

php artisan schedule:work

任务输出

Laravel 调度器提供了若干便捷方法,可用于处理定时任务生成的输出。首先,使用 sendOutputTo 方法,可将输出内容保存到文件中,以便后续查看:

use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')
    ->daily()
    ->sendOutputTo($filePath);

如果你想将输出内容追加到指定文件,可以使用 appendOutputTo 方法:

Schedule::command('emails:send')
    ->daily()
    ->appendOutputTo($filePath);

使用 emailOutputTo 方法,可将输出内容发送到你指定的邮箱。在发送任务输出前,需要先配置 Laravel 的邮件服务:

Schedule::command('report:generate')
    ->daily()
    ->sendOutputTo($filePath)
    ->emailOutputTo('[email protected]');

如果你希望仅当定时的 Artisan 命令或系统命令以非零退出码结束时,才发送输出邮件,可以使用 emailOutputOnFailure 方法:

Schedule::command('report:generate')
    ->daily()
    ->emailOutputOnFailure('[email protected]');

emailOutputTo、emailOutputOnFailure、sendOutputTo 和 appendOutputTo 方法仅适用于 command 和 exec 方法。

任务钩子

使用 before 和 after 方法,可以指定在定时任务执行前和执行后需要运行的代码:

use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')
    ->daily()
    ->before(function () {
        // 任务即将执行……
    })
    ->after(function () {
        // 任务已执行完毕……
    });

通过 onSuccess 和 onFailure 方法,可以指定定时任务执行成功或失败时需要运行的代码。任务执行失败指的是定时的 Artisan 命令或系统命令以非零退出码结束:

Schedule::command('emails:send')
    ->daily()
    ->onSuccess(function () {
        // 任务执行成功……
    })
    ->onFailure(function () {
        // 任务执行失败……
    });

如果命令产生了输出,你可以通过在钩子的闭包定义中,将 $output 参数的类型提示设为 Illuminate\Support\Stringable 实例,在 after、onSuccess 或 onFailure 钩子中访问该输出:

use Illuminate\Support\Stringable;

Schedule::command('emails:send')
    ->daily()
    ->onSuccess(function (Stringable $output) {
        // 任务执行成功……
    })
    ->onFailure(function (Stringable $output) {
        // 任务执行失败……
    });

请求 URL

使用 pingBefore 和 thenPing 方法,调度器能够在任务执行前或执行后自动对指定 URL 发起请求。该方法可用于通知外部服务(如Envoyer),告知其定时任务即将开始执行或已执行完毕:

Schedule::command('emails:send')
    ->daily()
    ->pingBefore($url)
    ->thenPing($url);

使用 pingOnSuccess 和 pingOnFailure 方法,可仅在任务执行成功或失败时对指定 URL 发起请求。任务执行失败指的是定时的 Artisan 命令或系统命令以非零退出码结束:

Schedule::command('emails:send')
    ->daily()
    ->pingOnSuccess($successUrl)
    ->pingOnFailure($failureUrl);

pingBeforeIf、thenPingIf、pingOnSuccessIf 和 pingOnFailureIf 方法可用于仅当指定条件为 true 时,对给定 URL 发起请求:

Schedule::command('emails:send')
    ->daily()
    ->pingBeforeIf($condition, $url)
    ->thenPingIf($condition, $url);

Schedule::command('emails:send')
    ->daily()
    ->pingOnSuccessIf($condition, $successUrl)
    ->pingOnFailureIf($condition, $failureUrl);

事件

Laravel 在调度过程中会触发多种事件。你可以为以下任意事件定义监听器:

事件名称
Illuminate\Console\Events\ScheduledTaskStarting
Illuminate\Console\Events\ScheduledTaskFinished
Illuminate\Console\Events\ScheduledBackgroundTaskFinished
Illuminate\Console\Events\ScheduledTaskSkipped
Illuminate\Console\Events\ScheduledTaskFailed