Skip to content

任务调度

介绍

过去,您可能已经为需要在服务器上调度的每个任务编写了一个 cron 配置条目。但是,这很快就会变得很痛苦,因为您的任务计划不再处于源代码控制中,您必须通过 SSH 连接到服务器才能查看现有的 cron 条目或添加其他条目。

Laravel 的命令调度程序提供了一种管理服务器上的计划任务的新方法。调度程序允许您在 Laravel 应用程序本身中流利而富有表现力地定义命令调度。使用调度程序时,您的服务器上只需要一个 cron 条目。您的任务计划通常在应用程序的 routes/console.php 文件中定义。

定义计划

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

php
    <?php
    
    use Illuminate\Support\Facades\DB;
    use Illuminate\Support\Facades\Schedule;
    
    Schedule::call(function () {
        DB::table('recent_users')->delete();
    })->daily();

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

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

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

php
    use Illuminate\Console\Scheduling\Schedule;
    
    ->withSchedule(function (Schedule $schedule) {
        $schedule->call(new DeleteRecentUsers)->daily();
    })

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

bash
php artisan schedule:list

调度 Artisan 命令

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

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

php
    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 命令,可以在命令定义之后链接与调度相关的方法:

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

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

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

计划排队的作业

job 方法可用于计划排队的 job。此方法提供了一种方便的方式来调度排队的作业,而无需使用 call 方法定义闭包以将作业排队:

php
    use App\Jobs\Heartbeat;
    use Illuminate\Support\Facades\Schedule;
    
    Schedule::job(new Heartbeat)->everyFiveMinutes();

可选的第二个和第三个参数可以提供给 job 方法,该方法指定应该用于对 job 进行排队的队列名称和队列连接:

php
    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 方法可用于向操作系统发出命令:

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

计划频率选项

我们已经看到了一些示例,说明如何将任务配置为按指定的时间间隔运行。但是,您可以为任务分配更多的任务计划频率:

方法描述
->cron('* * * * *');按自定义 cron 计划运行任务。
->everySecond();每秒运行一次任务。
->everyTwoSeconds();每 2 秒运行一次任务。
->everyFiveSeconds();每 5 秒运行一次任务。
->everyTenSeconds();每 10 秒运行一次任务。
->everyFifteenSeconds();每 15 秒运行一次任务。
->everyTwentySeconds();每 20 秒运行一次任务。
->everyThirtySeconds();每 30 秒运行一次任务。
->everyMinute();每分钟运行一次任务。
->everyTwoMinutes();每 2 分钟运行一次任务。
->everyThreeMinutes();每 3 分钟运行一次任务。
->everyFourMinutes();每 4 分钟运行一次任务。
->everyFiveMinutes();每 5 分钟运行一次任务。
->everyTenMinutes();每 10 分钟运行一次任务。
->everyFifteenMinutes();每 15 分钟运行一次任务。
->everyThirtyMinutes();每 30 分钟运行一次任务。
->hourly();每小时运行一次任务。
->hourlyAt(17);每小时运行一次任务,每小时过小时 17 分钟。
->everyOddHour($minutes = 0);每奇数小时运行一次任务。
->everyTwoHours($minutes = 0);每 2 小时运行一次任务。
->everyThreeHours($minutes = 0);每 3 小时运行一次任务。
->everyFourHours($minutes = 0);每 4 小时运行一次任务。
->everySixHours($minutes = 0);每 6 小时运行一次任务。
->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');设置任务的时区。

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

php
    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);将任务限制为特定环境。

Day 约束

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

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

或者,在定义任务应运行的日期时,您可以使用 Illuminate\Console\Scheduling\Schedule 类上可用的常量:

php
    use Illuminate\Support\Facades;
    use Illuminate\Console\Scheduling\Schedule;
    
    Facades\Schedule::command('emails:send')
                    ->hourly()
                    ->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);

时间约束之间

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

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

同样,unlessBetween 方法可用于在一段时间内排除任务的执行:

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

真值测试约束

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

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

skip 方法可以看作是 when 的反面。如果 skip 方法返回 true,则不会执行定时任务:

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

当使用链式 when 方法时,scheduled 命令将仅在所有 when 条件都返回 true 时执行。

环境约束

environments 方法只能用于在给定环境(由 APP_ENV环境变量定义)上执行任务:

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

时区

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

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

如果您重复为所有计划任务分配相同的时区,则可以通过在应用程序的应用程序配置文件中定义 schedule_timezone 选项来指定应为所有计划分配哪个时区:

php
    'timezone' => env('APP_TIMEZONE', 'UTC'),
    
    'schedule_timezone' => 'America/Chicago',

WARNING

请记住,某些时区使用夏令时。当夏令时发生更改时,您的计划任务可能会运行两次,甚至根本不运行。因此,我们建议尽可能避免时区计划。

防止任务重叠

默认情况下,即使任务的上一个实例仍在运行,也会运行计划任务。为了防止这种情况,你可以使用 withoutOverlapping 方法:

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

在此示例中,如果 emails:sendArtisan 命令尚未运行,则每分钟运行一次。如果任务的执行时间差异很大,则无法准确预测给定任务将花费多长时间,则 withoutOverlapping 方法特别有用。

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

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

在后台,withoutOverlapping 方法利用应用程序的缓存来获取锁。如有必要,您可以使用 schedule:clear-cache Artisan 命令清除这些缓存锁。通常,仅当任务由于意外的服务器问题而卡住时,才需要这样做。

在一台服务器上运行任务

WARNING

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

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

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

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

命名单个服务器作业

有时,您可能需要安排使用不同的参数分派相同的作业,同时仍指示 Laravel 在单个服务器上运行作业的每个排列。为此,您可以通过 name 方法为每个计划定义分配一个唯一的名称:

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

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

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

后台任务

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

php
    use Illuminate\Support\Facades\Schedule;
    
    Schedule::command('analytics:report')
             ->daily()
             ->runInBackground();

WARNING

runInBackground 方法只能在通过 commandexec 方法调度任务时使用。

维护模式

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

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

运行调度程序

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

因此,当使用 Laravel 的调度器时,我们只需要向我们的服务器添加一个 cron 配置条目,该条目每分钟运行 schedule:run 命令。如果您不知道如何将 cron 条目添加到您的服务器,请考虑使用诸如 Laravel Forge 之类的服务,它可以为您管理 cron 条目:

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

分钟级计划任务

在大多数操作系统上,cron 作业限制为每分钟最多运行一次。但是,Laravel 的调度器允许您安排任务以更频繁的时间间隔运行,甚至每秒运行一次:

php
    use Illuminate\Support\Facades\Schedule;
    
    Schedule::call(function () {
        DB::table('recent_users')->delete();
    })->everySecond();

在应用程序中定义亚分钟任务时,schedule:run 命令将继续运行,直到当前分钟结束,而不是立即退出。这允许该命令在一分钟内调用所有必需的亚分钟任务。

由于运行时间超过预期的亚分钟任务可能会延迟后续亚分钟任务的执行,因此建议所有亚分钟任务都调度排队作业或后台命令来处理实际任务处理:

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

中断 Sub-Minute 任务

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

要中断正在进行的 schedule:run 调用,您可以将 schedule:interrupt 命令添加到应用程序的部署脚本中。应在应用程序完成部署后调用此命令:

shell
php artisan schedule:interrupt

在本地运行调度程序

通常,您不会将调度程序 cron 条目添加到本地开发计算机。相反,您可以使用 schedule:work Artisan 命令。此命令将在前台运行,并每分钟调用一次调度程序,直到您终止该命令:

shell
php artisan schedule:work

任务输出

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

php
    use Illuminate\Support\Facades\Schedule;
    
    Schedule::command('emails:send')
             ->daily()
             ->sendOutputTo($filePath);

如果您想将输出附加到给定文件,您可以使用 appendOutputTo 方法:

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

使用 emailOutputTo 方法,您可以通过电子邮件将输出发送到您选择的电子邮件地址。在通过电子邮件发送任务的输出之前,您应该配置 Laravel 的电子邮件服务

php
    Schedule::command('report:generate')
             ->daily()
             ->sendOutputTo($filePath)
             ->emailOutputTo('taylor@example.com');

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

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

WARNING

emailOutputToemailOutputOnFailuresendOutputToappendOutputTo 方法专用于 commandexec 方法。

任务钩子

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

php
    use Illuminate\Support\Facades\Schedule;
    
    Schedule::command('emails:send')
             ->daily()
             ->before(function () {
                 // The task is about to execute...
             })
             ->after(function () {
                 // The task has executed...
             });

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

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

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

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

Pinging URLs

使用 pingBeforethenPing 方法,计划程序可以在执行任务之前或之后自动 ping 给定的 URL。此方法可用于通知外部服务(例如 Envoyer)您的计划任务正在开始或已完成执行:

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

仅当给定条件为 true 时,pingBeforeIfthenPingIf 方法才能用于 ping 给定的 URL:

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

仅当任务成功或失败时,pingOnSuccesspingOnFailure 方法才可用于对给定 URL 执行 ping 操作。失败表示计划的 Artisan 或 system 命令以非零退出代码终止:

php
    Schedule::command('emails:send')
             ->daily()
             ->pingOnSuccess($successUrl)
             ->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