简介

过去,你可能需要在服务器上为每一个调度任务去创建一个 cron 配置项。但是这种方式很快会变得不友好,因为这些任务调度已不在源代码控制中,并且你每次都需要通过 SSH 链接登录到你的服务器中查看现有的 cron 条目或添加其他配置项。

Laravel 的命令调度器提供了一种全新的方法来管理服务器上的定时任务。调度器允许你在 Laravel 应用中流畅地定义命令调度。使用调度器时,只需要你服务器上一个单一的 cron 配置项。你的任务调度通常在应用程序的 php routes/console.php 文件中定义。

定义调度

你可以在应用程序的 php routes/console.php 文件中定义所有计划任务。开始前,我们先看一个示例。在这个例子中,我们将安排每天午夜调用一个闭包。在闭包中,我们将执行一个数据库查询来清除一个表:

  1. <?php
  2. use Illuminate\Support\Facades\DB;
  3. use Illuminate\Support\Facades\Schedule;
  4. Schedule::call(function () {
  5. DB::table('recent_users')->delete();
  6. })->daily();

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

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

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

  1. use Illuminate\Console\Scheduling\Schedule;
  2. ->withSchedule(function (Schedule $schedule) {
  3. $schedule->call(new DeleteRecentUsers)->daily();
  4. })

如果你想查看调度任务的概览和下一次运行时间,可以使用 php schedule:list Artisan 命令:

  1. php artisan schedule:list

调度 Artisan 命令

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

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

  1. use App\Console\Commands\SendEmailsCommand;
  2. use Illuminate\Support\Facades\Schedule;
  3. Schedule::command('emails:send Taylor --force')->daily();
  4. Schedule::command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();

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

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

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

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

调度队列作业

php job 方法可用于调度 队列作业。该方法为调度队列作业提供了一种便捷的方式,而无需使用 php call 方法定义闭包来调度队列作业:

  1. use App\Jobs\Heartbeat;
  2. use Illuminate\Support\Facades\Schedule;
  3. Schedule::job(new Heartbeat)->everyFiveMinutes();

可为 php job 方法提供可选的第二和第三个参数,用于指定队列名称和队列连接,以便对作业进行排序:

  1. use App\Jobs\Heartbeat;
  2. use Illuminate\Support\Facades\Schedule;
  3. // 将任务分派到 「sqs」连接上的「心跳」队列...
  4. Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();

调度 Shell 命令

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

  1. use Illuminate\Support\Facades\Schedule;
  2. Schedule::exec('node /home/forge/script.js')->daily();

调度频率选项

我们已经举例说明了如何配置任务在指定的时间间隔内运行。然而,你还可以为任务分配更多的任务调度频率:

Method Description
->cron('* * * * *'); 按自定义 Cron 计划运行任务
->everySecond(); 每秒运行一次任务
->everyTwoSeconds(); 每两秒运行一次任务
->everyFiveSeconds(); 每五秒运行一次任务
->everyTenSeconds(); 每十秒运行一次任务
->everyFifteenSeconds(); 每十五秒运行一次任务
->everyTwentySeconds(); 每二十秒运行一次任务
->everyThirtySeconds(); 每三十秒运行一次任务
->everyMinute(); 每分钟运行一次任务
->everyTwoMinutes(); 每两分钟运行一次任务
->everyThreeMinutes(); 每三分钟运行一次任务
->everyFourMinutes(); 每四分钟运行一次任务
->everyFiveMinutes(); 每五分钟运行一次任务
->everyTenMinutes(); 每十分钟运行一次任务
->everyFifteenMinutes(); 每十五分钟运行一次任务
->everyThirtyMinutes(); 每三十分钟运行一次任务
->hourly(); 每小时运行一次任务
->hourlyAt(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 运行一次任务
->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'); 设置任务时区
  1. use Illuminate\Support\Facades\Schedule;
  2. // 每周运行一次,时间为周一下午 1 点...
  3. Schedule::call(function () {
  4. // ...
  5. })->weekly()->mondays()->at('13:00');
  6. // 在工作日上午 8 点至下午 5 点之间,每小时运行一次...
  7. Schedule::command('foo')
  8. ->weekdays()
  9. ->hourly()
  10. ->timezone('America/Chicago')
  11. ->between('8:00', '17:00');

其他时间约束列表如下:

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

日约束
php days 方法可以用于约束任务在每周的指定日期执行。举个例子,你可以调度命令在星期天和星期三每小时运行一次:

  1. use Illuminate\Support\Facades\Schedule;
  2. Schedule::command('emails:send')
  3. ->hourly()
  4. ->days([0, 3]);

不仅如此,你还可以使用 php Illuminate\Console\Scheduling\Schedule 类中的常量来设置任务在指定日期运行:

  1. use Illuminate\Support\Facades;
  2. use Illuminate\Console\Scheduling\Schedule;
  3. Facades\Schedule::command('emails:send')
  4. ->hourly()
  5. ->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);

时间范围限制
php between 方法可用于限制任务在一天中的某个时间段执行:

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

同样,php unlessBetween 方法也可用于限制任务不在一天中的某个时间段执行:

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

真值检测限制
php when 方法可根据闭包返回结果来执行任务。换言之,若给定的闭包返回 php true,若无其他限制条件阻止,任务就会一直执行:

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

php skip 可看作是 php when 的逆方法。若 php skip 方法返回 php true,任务将不会执行:

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

当链式调用 php when 方法时,仅当所有php when 都返回 php true 时,任务才会执行。

环境限制
php environments 方法可用于指定任务的执行环境(由 php APP_ENV 环境变量定义):

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

时区

使用 php timezone 方法,你可以指定计划任务的时间转化为给定的时区:

  1. use Illuminate\Support\Facades\Schedule;
  2. Schedule::command('report:generate')
  3. ->timezone('America/New_York')
  4. ->at('2:00')

如果你要重复为所有计划任务分配相同的时区,可以在应用程序的 php app 配置文件中定义一个 php schedule_timezone 选项,指定应将哪个时区分配给所有计划任务:

  1. 'timezone' => env('APP_TIMEZONE', 'UTC'),
  2. 'schedule_timezone' => 'America/Chicago',


[!WARNING]
请记住,有些时区使用夏令时。当夏令时发生变化时,你的计划任务可能会运行两次,甚至根本不会运行。因此,我们建议尽可能避免时区调度。

避免任务重复

默认情况下,即使之前的任务实例还在执行,计划内的任务也会执行。为避免这种情况的发生,你可以使用 php withoutOverlapping 方法:

  1. use Illuminate\Support\Facades\Schedule;
  2. Schedule::command('emails:send')->withoutOverlapping();

在此例中,若 php emails:send Artisan 命令 尚未运行,则它将会每分钟执行一次。如果任务的执行时间非常不确定,导致你无法准确预测任务的执行时间,那么 php withoutOverlapping 方法就特别有用。

如有需要,你可以在 php withoutOverlapping 锁过期之前,指定它的过期分钟数。默认情况下,这个锁会在 24 小时后过期:

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

在幕后,withoutOverlapping 方法使用应用程序的 cache 来获取锁。如果必要,你可以使用 php schedule:clear-cache Artisan 命令清除这些缓存锁。通常只有在服务器出现意外问题导致任务卡住时才需要这样做。

任务只运行在一台服务器上


[!WARNING]
要使用此功能,你的应用程序必须使用 php databasephp memcachedphp dynamodbphp redis 缓存驱动程序作为应用程序的默认缓存驱动程序。此外,所有服务器都必须与同一个中央缓存服务器通信。


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

要表示任务仅在一台服务器上运行,请在定义计划任务时使用 php onOneServer 方法。第一个获得任务的服务器将确保任务的原子锁,以防止其他服务器同时运行同一任务:

  1. use Illuminate\Support\Facades\Schedule;
  2. Schedule::command('report:generate')
  3. ->fridays()
  4. ->at('17:00')
  5. ->onOneServer();

为单服务器任务命名
有时,你可能需要使用不同的参数调度相同的作业,同时指示 Laravel 在单服务器上运行每个作业的每种排列。要做到这一点,你可以通过 php name 方法为每个计划定义分配唯一名称:

  1. Schedule::job(new CheckUptime('https://laravel.com'))
  2. ->name('check_uptime:laravel.com')
  3. ->everyFiveMinutes()
  4. ->onOneServer();
  5. Schedule::job(new CheckUptime('https://vapor.laravel.com'))
  6. ->name('check_uptime:vapor.laravel.com')
  7. ->everyFiveMinutes()
  8. ->onOneServer();

同样,如果打算在一台服务器上运行计划闭包,则必须为其分配一个名称:

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

后台任务

默认情况下,计划在同一时间执行的多个任务将根据你的 php schedule 方法中定义的顺序依次执行。如果你有长时间运行的任务,将会导致后续任务比预期时间更晚启动。如果你想在后台运行任务,以便它们同时运行,你可以使用 php runInBackground 方法:

  1. use Illuminate\Support\Facades\Schedule;
  2. Schedule::command('analytics:report')
  3. ->daily()
  4. ->runInBackground();


[!WARNING]
php runInBackground 方法只能在通过 php commandphp exec 方法调度任务时使用。

维护模式

当应用程序处于维护模式时,你的应用程序计划任务将不会运行,因为我们不想调度任务干扰到服务器上可能还未完成的维护项目。然而,如果你想强制任务在维护模式下运行,你可以使用 php evenInMaintenanceMode 方法:

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

运行调度器

现在,我们已经学会了如何定义计划任务,接下来让我们讨论如何真正在服务器上运行它们。php schedule:run Artisan 命令将评估你的所有计划任务,并根据服务器的当前时间决定它们是否运行。

因此,当使用 Laravel 的调度器时,我们只需要在服务器上添加一个单一的 cron 配置项,每分钟运行一次 php schedule:run 命令。如果你不知道如何向服务器添加 cron 条目,请考虑使用诸如 Laravel Forge 之类的服务来为你管理 cron 配置项:

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

小于一分钟的预定任务

在大多数操作系统上,cron 作业被限制为每分钟最多运行一次。然而,Laravel 的调度程序允许你安排任务以更频繁的间隔运行,甚至频繁到每秒一次:

  1. use Illuminate\Support\Facades\Schedule;
  2. Schedule::call(function () {
  3. DB::table('recent_users')->delete();
  4. })->everySecond();

当您的应用程序中定义了小于一分钟的任务时,php schedule:run 命令将继续运行到当前分钟结束而不是立即退出。这允许命令在整个分钟内调用所有需要的小于一分钟的任务。

由于小于一分钟的任务如果运行时间超出预期可能会延迟后续小于一分钟任务的执行,建议所有小于一分钟的任务派发队列作业或后台命令来负责实际的任务处理:

  1. use App\Jobs\DeleteRecentUsers;
  2. Schedule::job(new DeleteRecentUsers)->everyTenSeconds();
  3. Schedule::command('users:delete')->everyTenSeconds()->runInBackground();

中断小于一分钟的任务
由于在定义了小于一分钟的任务时,php schedule:run 命令会在调用的整分钟内运行,因此有时可能需要在部署应用程序时中断该命令。否则,已在运行的 php schedule:run 命令实例将继续使用你的应用程序先前部署的代码,直到当前分钟结束。

为了中断正在进行的 php schedule:run 调用,你可添加 php schedule:interrupt 命令到应用程序的部署脚本中。该命令应在你的应用程序部署完成后调用:

  1. php artisan schedule:interrupt

本地运行调度器

通常,你不会在本地开发机器上添加调度器 cron 条目。相反,你可以使用 php schedule:work Artisan 命令。此命令将在前台运行,并在你终止命令之前每分钟调用一次调度器:

  1. php artisan schedule:work

任务输出

Laravel 调度器提供了几种方便的方法来处理计划任务生成的输出。首先,使用 php sendOutputTo 方法,你可以将输出发送到文件,以供日后检查:

  1. use Illuminate\Support\Facades\Schedule;
  2. Schedule::command('emails:send')
  3. ->daily()
  4. ->sendOutputTo($filePath);

如果你希望将输出追加到给定文件,你可以使用 php appendOutputTo 方法:

  1. Schedule::command('emails:send')
  2. ->daily()
  3. ->appendOutputTo($filePath);

使用 php emailOutputTo 方法, 你可以把输出结果电邮到你选择的电邮地址。在发送任务的输出结果之前,你应该配置 Laravel 的 电子邮件服务:

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

如果你只想在预定的 Artisan 或系统命令以非零退出代码终止时通过电子邮件发送输出,请使用 php emailOutputOnFailure 方法:

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


[!WARNING]
php emailOutputTophp emailOutputOnFailurephp sendOutputTophp appendOutputTo 方法是 php commandphp exec 方法所独有的。

任务钩子

使用 php beforephp after 方法,你可以指定在计划任务执行前后要执行的代码:

  1. use Illuminate\Support\Facades\Schedule;
  2. Schedule::command('emails:send')
  3. ->daily()
  4. ->before(function () {
  5. // 任务即将执行...
  6. })
  7. ->after(function () {
  8. // 任务已执行...
  9. });

php onSuccessphp onFailure 方法允许你指定在计划任务成功或失败时执行的代码。失败表示预定的 Artisan 或系统命令以非零的退出代码终止:

  1. Schedule::command('emails:send')
  2. ->daily()
  3. ->onSuccess(function () {
  4. // 任务成功...
  5. })
  6. ->onFailure(function () {
  7. // 任务失败...
  8. });

如果命令中提供了输出,你可以在 php afterphp onSuccessphp onFailure 钩子中访问它,方法是将 php Illuminate\Support\Stringable 实例作为钩子闭包定义的 php $output 参数进行类型提示:

  1. use Illuminate\Support\Stringable;
  2. Schedule::command('emails:send')
  3. ->daily()
  4. ->onSuccess(function (Stringable $output) {
  5. // 任务成功...
  6. })
  7. ->onFailure(function (Stringable $output) {
  8. // 任务失败...
  9. });

Ping 网址
使用 php pingBeforephp thenPing 方法,调度器可以在任务执行前后自动 ping 给定的网址。此方法对于通知你计划任务正在开始或已完成执行的外部服务(如 Envoyer)非常有用:

  1. Schedule::command('emails:send')
  2. ->daily()
  3. ->pingBefore($url)
  4. ->thenPing($url);

php pingBeforeIfphp thenPingIf 方法可用于仅在给定条件为 php true 时 ping 给定网址:

  1. Schedule::command('emails:send')
  2. ->daily()
  3. ->pingBeforeIf($condition, $url)
  4. ->thenPingIf($condition, $url);

php pingOnSuccessphp pingOnFailure 方法可用于仅在任务成功或失败时 ping 给定的网址。失败表示预定的 Artisan 或系统命令以非零退出代码终止:

  1. Schedule::command('emails:send')
  2. ->daily()
  3. ->pingOnSuccess($successUrl)
  4. ->pingOnFailure($failureUrl);

事件

Laravel 在调度过程中发送各种事件。你可以为以下事件定义监听器:

Event Name
Illuminate\Console\Events\ScheduledTaskStarting
Illuminate\Console\Events\ScheduledTaskFinished
Illuminate\Console\Events\ScheduledBackgroundTaskFinished
Illuminate\Console\Events\ScheduledTaskSkipped
Illuminate\Console\Events\ScheduledTaskFailed