跳到内容

任务调度

介绍

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

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

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 运行任务。
->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' => env('APP_TIMEZONE', 'UTC'),
 
'schedule_timezone' => 'America/Chicago',
exclamation

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

防止任务重叠

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

在一台服务器上运行任务

exclamation

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

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

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

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

命名单服务器作业

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

Schedule::job(new CheckUptime('https://laravel.net.cn'))
->name('check_uptime:laravel.com')
->everyFiveMinutes()
->onOneServer();
 
Schedule::job(new CheckUptime('https://vapor.laravel.net.cn'))
->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();
exclamation

仅当通过 commandexec 方法计划任务时,才能使用 runInBackground 方法。

维护模式

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

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

计划组

当定义具有相似配置的多个计划任务时,您可以使用 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 的调度程序时,我们只需要在我们的服务器上添加一个 crontab 配置条目,该条目每分钟运行一次 schedule:run 命令。 如果您不知道如何将 crontab 条目添加到您的服务器,请考虑使用 Laravel Forge 等服务,它可以为您管理 crontab 条目

* * * * * 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

在本地运行调度器

通常,您不会将调度程序 crontab 条目添加到您的本地开发机器中。 相反,您可以使用 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]');
exclamation

emailOutputToemailOutputOnFailuresendOutputToappendOutputTo 方法是 commandexec 方法专有的。

任务钩子

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

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 或系统命令以非零退出代码终止

Schedule::command('emails:send')
->daily()
->onSuccess(function () {
// The task succeeded...
})
->onFailure(function () {
// The task failed...
});

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

use Illuminate\Support\Stringable;
 
Schedule::command('emails:send')
->daily()
->onSuccess(function (Stringable $output) {
// The task succeeded...
})
->onFailure(function (Stringable $output) {
// The task failed...
});

Ping URL

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

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

仅当任务成功或失败时,才能使用 pingOnSuccesspingOnFailure 方法 ping 给定的 URL。 失败表示计划的 Artisan 或系统命令以非零退出代码终止

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

仅当给定条件为 true 时,才能使用 pingBeforeIfthenPingIfpingOnSuccessIfpingOnFailureIf 方法 ping 给定的 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