跳至内容

任务调度

介绍

过去,您可能为每个需要在服务器上安排的任务都编写了 cron 配置条目。然而,这很快就会变得麻烦,因为您的任务计划不再受源代码控制,您必须通过 SSH 登录服务器才能查看现有的 cron 条目或添加其他条目。

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

定义时间表

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

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

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

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

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

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

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

1php artisan schedule:list

调度 Artisan 命令

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

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

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

调度 Artisan 关闭命令

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

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

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

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

调度队列作业

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

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

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

1use App\Jobs\Heartbeat;
2use Illuminate\Support\Facades\Schedule;
3 
4// Dispatch the job to the "heartbeats" queue on the "sqs" connection...
5Schedule::job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();

调度 Shell 命令

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

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::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(); 每季度第一天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'); 设置任务的时区。

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

1use Illuminate\Support\Facades\Schedule;
2 
3// Run once per week on Monday at 1 PM...
4Schedule::call(function () {
5 // ...
6})->weekly()->mondays()->at('13:00');
7 
8// Run hourly from 8 AM to 5 PM on weekdays...
9Schedule::command('foo')
10 ->weekdays()
11 ->hourly()
12 ->timezone('America/Chicago')
13 ->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方法可用于将任务的执行限制在一周中的特定日期。例如,你可以安排命令在周日和周三每小时运行一次:

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

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

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

时间约束之间

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

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

类似地,该unlessBetween方法可用于在一段时间内排除某项任务的执行:

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

真值测试约束

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

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

skip方法可以看作是 的逆方法when。如果该skip方法返回true,则计划任务将不会执行:

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

当使用链式方法时,只有所有条件都返回时,when预定的命令才会执行whentrue

环境约束

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

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

时区

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

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

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

1'timezone' => 'UTC',
2 
3'schedule_timezone' => 'America/Chicago',

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

防止任务重叠

默认情况下,即使任务的前一个实例仍在运行,计划任务也会运行。为了避免这种情况,你可以使用以下withoutOverlapping命令:

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

在此示例中,如果emails:send Artisan 命令尚未运行,它将每分钟运行一次。withoutOverlapping如果您的任务执行时间变化很大,导致您无法准确预测给定任务的执行时间,则此方法尤其有用。

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

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

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

在一台服务器上运行任务

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

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

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

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

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

1Schedule::useCache('database');

命名单服务器作业

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

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

类似地,如果计划在一台服务器上运行预定的关闭操作,则必须为其分配一个名称:

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

后台任务

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

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

该方法仅可在通过和方法runInBackground调度任务时使用commandexec

维护模式

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

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

计划组

当定义多个具有相似配置的计划任务时,可以使用 Laravel 的任务分组功能,以避免为每个任务重复相同的设置。分组任务可以简化代码并确保相关任务之间的一致性。

要创建一组计划任务,请调用所需的任务配置方法,然后调用该group方法。该group方法接受一个闭包,该闭包负责定义共享指定配置的任务:

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::daily()
4 ->onOneServer()
5 ->timezone('America/New_York')
6 ->group(function () {
7 Schedule::command('emails:send --force');
8 Schedule::command('emails:prune');
9 });

运行调度程序

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

因此,在使用 Laravel 的调度程序时,我们只需在服务器中添加一个 cron 配置条目,即可schedule:run每分钟运行该命令。如果您不知道如何向服务器添加 cron 条目,可以考虑使用Laravel Cloud等托管平台,它可以为您管理计划任务的执行:

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

亚分​​钟级计划任务

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

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

当您在应用程序中定义了亚分钟级任务时,该schedule:run命令将持续运行直到当前分钟结束,而不是立即退出。这使得该命令能够在当前分钟内调用所有必需的亚分钟级任务。

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

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

打断一分钟内完成的任务

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

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

1php artisan schedule:interrupt

本地运行调度程序

通常,您不会在本地开发机器上添加调度程序 cron 条目。相反,您可以使用schedule:workArtisan 命令。此命令将在前台运行,并每分钟调用一次调度程序,直到您终止该命令。当定义了亚分钟级任务时,调度程序将在每分钟内持续运行以处理这些任务:

1php artisan schedule:work

任务输出

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

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

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

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

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

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

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

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

emailOutputTo方法是和方法独有emailOutputOnFailuresendOutputToappendOutputTocommandexec

任务钩子

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

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('emails:send')
4 ->daily()
5 ->before(function () {
6 // The task is about to execute...
7 })
8 ->after(function () {
9 // The task has executed...
10 });

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

1Schedule::command('emails:send')
2 ->daily()
3 ->onSuccess(function () {
4 // The task succeeded...
5 })
6 ->onFailure(function () {
7 // The task failed...
8 });

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

1use Illuminate\Support\Stringable;
2 
3Schedule::command('emails:send')
4 ->daily()
5 ->onSuccess(function (Stringable $output) {
6 // The task succeeded...
7 })
8 ->onFailure(function (Stringable $output) {
9 // The task failed...
10 });

Ping URL

使用pingBeforethenPing方法,调度程序可以在任务执行之前或之后自动 ping 指定的 URL。此方法对于通知外部服务(例如Envoyer)你的调度任务即将开始或已完成执行非常有用:

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

pingOnSuccess方法pingOnFailure仅当任务成功或失败时才可用于 ping 给定的 URL。失败表示已调度的 Artisan 或系统命令以非零退出代码终止:

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

pingBeforeIf仅当满足以下给定条件时,才可使用、、和方法来 ping 给thenPingIfpingOnSuccessIfURL pingOnFailureIftrue

1Schedule::command('emails:send')
2 ->daily()
3 ->pingBeforeIf($condition, $url)
4 ->thenPingIf($condition, $url);
5 
6Schedule::command('emails:send')
7 ->daily()
8 ->pingOnSuccessIf($condition, $successUrl)
9 ->pingOnFailureIf($condition, $failureUrl);

Events

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

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