laravel
5. 深入探讨
任务调度

介绍

过去,您可能需要为服务器上需要调度的每个任务编写一个 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();

除了使用闭包进行调度外,您还可以调度可调用对象 (opens in a new tab)。可调用对象是包含 __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('删除最近的用户')->daily();

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

Artisan::command('emails:send {user} {--force}', function ($user) {
    //...
})->purpose('向指定用户发送电子邮件')->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;

// 将任务分发到 "sqs" 连接上的 "heartbeats" 队列中...
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 运行任务。
->weekly();每周日 00:00 运行任务。
->weeklyOn(1, '8:00');每周一 8:00 运行任务。
->monthly();每月 1 日 00:00 运行任务。
->monthlyOn(4, '15:00');每月 4 日 15:00 运行任务。
->twiceMonthly(1, 16, '13:00');每月 1 日和 16 日 13:00 运行任务。
->lastDayOfMonth('15:00');每月最后一天 15:00 运行任务。
->quarterly();每季度 1 日 00:00 运行任务。
->quarterlyOn(4, '14:00');每季度 4 日 14:00 运行任务。
->yearly();每年 1 日 00:00 运行任务。
->yearlyOn(6, 1, '17:00');每年 6 月 1 日 17:00 运行任务。
->timezone('America/New_York');为任务设置时区。

这些方法可以与其他约束条件结合使用,以创建更精细调整的调度计划,使其仅在一周的某些天运行。例如,您可以安排一个命令在每周一运行:

use Illuminate\Support\Facades\Schedule;

// 每周一下午 1 点运行一次...
Schedule::call(function () {
    //...
})->weekly()->mondays()->at('13:00');

// 工作日从上午 8 点到下午 5 点每小时运行一次...
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' => env('APP_TIMEZONE', 'UTC'),
 
'schedule_timezone' => 'America/Chicago',

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

防止任务重叠

默认情况下,即使任务的前一个实例仍在运行,计划任务也会运行。为了防止这种情况,您可以使用 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 命令清除这些缓存锁。这通常仅在由于意外的服务器问题导致任务卡住时才需要。

在一台服务器上运行任务

[!WARNING]
要使用此功能,您的应用程序必须将 databasememcacheddynamodbredis 缓存驱动程序作为应用程序的默认缓存驱动程序。此外,所有服务器必须与同一中央缓存服务器进行通信。

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

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

use Illuminate\Support\Facades\Schedule;
 
Schedule::command('report:generate')
         ->fridays()
         ->at('17:00')
         ->onOneServer();

命名单服务器任务

有时,您可能需要安排相同的任务以不同的参数进行调度,同时仍然指示 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();

[!警告]
runInBackground 方法仅可在通过 commandexec 方法安排任务时使用。

维护模式

当您的应用程序处于维护模式时,您的应用程序的计划任务将不会运行,因为我们不希望您的任务干扰您可能在服务器上执行的任何未完成的维护。但是,如果您希望强制任务即使在维护模式下也能运行,您可以在定义任务时调用 evenInMaintenanceMode 方法:

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

运行调度器

现在我们已经了解了如何定义计划任务,让我们讨论如何在我们的服务器上实际运行它们。schedule:run Artisan 命令将评估您的所有计划任务,并根据服务器的当前时间确定它们是否需要运行。

因此,当使用 Laravel 的调度器时,我们只需要在我们的服务器上添加一个 cron 配置条目,每分钟运行一次 schedule:run 命令。如果您不知道如何向您的服务器添加 cron 条目,可以考虑使用像 Laravel Forge (opens in a new tab) 这样的服务,它可以为您管理 cron 条目:

* * * * * 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('taylor@example.com');

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

Schedule::command('report:generate')
         ->daily()
         ->emailOutputOnFailure('taylor@example.com');

[!警告]
emailOutputToemailOutputOnFailuresendOutputToappendOutputTo 方法仅适用于 commandexec 方法。

任务钩子

使用 beforeafter 方法,您可以指定在计划任务执行之前和之后要执行的代码:

use Illuminate\Support\Facades\Schedule;

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

onSuccessonFailure 方法允许您指定如果计划任务成功或失败时要执行的代码。失败表示计划的 Artisan 或系统命令以非零退出代码终止:

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

如果您的命令有可用的输出,您可以在您的 afteronSuccessonFailure 钩子中通过将 Illuminate\Support\Stringable 实例作为钩子闭包定义的 $output 参数进行类型提示来访问它:

use Illuminate\Support\Stringable;

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

探测 URL

使用 pingBeforethenPing 方法,调度器可以在任务执行之前或之后自动探测给定的 URL。此方法对于通知外部服务(例如 Envoyer (opens in a new tab))您的计划任务已开始或已完成执行非常有用:

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

pingBeforeIfthenPingIf 方法可用于仅在给定条件为 true 时探测给定的 URL:

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

pingOnSuccesspingOnFailure 方法可用于仅在任务成功或失败时探测给定的 URL。失败表示计划的 Artisan 或系统命令以非零退出代码终止:

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

事件

在调度过程中,Laravel 会分发各种事件。您可以为以下任何事件定义监听器

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