任务调度
简介
过去,你可能需要为服务器上计划的每个任务编写一个 cron 配置条目。然而,这很快就会变得很麻烦,因为你的任务调度不再受版本控制,而且你必须通过 SSH 连接到服务器才能查看现有的 cron 条目或添加新条目。
Laravel 的命令调度程序为管理服务器上的定时任务提供了一种全新的方法。调度程序允许你在 Laravel 应用程序本身中流畅且富有表现力地定义命令调度。使用调度程序时,服务器上只需要一个 cron 条目。你的任务调度通常定义在应用程序的 routes/console.php 文件中。
定义调度
你可以在应用程序的 routes/console.php 文件中定义所有定时任务。首先,让我们看一个例子。在这个例子中,我们将调度一个闭包,使其每天午夜执行。在闭包内部,我们将执行数据库查询来清空一张表。
1<?php2 3use Illuminate\Support\Facades\DB;4use Illuminate\Support\Facades\Schedule;5 6Schedule::call(function () {7 DB::table('recent_users')->delete();8})->daily();
除了使用闭包进行调度外,你还可以调度 可调用对象 (invokable objects)。可调用对象是包含 __invoke 方法的简单 PHP 类。
1Schedule::call(new DeleteRecentUsers)->daily();
如果你更喜欢将 routes/console.php 文件仅用于命令定义,你可以使用应用程序 bootstrap/app.php 文件中的 withSchedule 方法来定义定时任务。此方法接收一个包含调度程序实例的闭包。
1use Illuminate\Console\Scheduling\Schedule;2 3->withSchedule(function (Schedule $schedule) {4 $schedule->call(new DeleteRecentUsers)->daily();5})
如果你想查看定时任务的概览以及它们下一次计划运行的时间,可以使用 schedule:list Artisan 命令。
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 运行一次任务。 |
->daysOfMonth([1, 10, 20]); |
在每月的特定日期运行任务。 |
->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 方法时,调度命令仅在所有 when 条件都返回 true 时执行。
环境约束
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')
如果你反复为所有定时任务分配相同的时区,可以通过在应用程序的 app 配置文件中定义 schedule_timezone 选项,来指定应分配给所有调度的时区。
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-cache Artisan 命令清除这些缓存锁。这通常仅在任务因意外的服务器问题而卡住时才需要。
在单台服务器上运行任务
要使用此功能,你的应用程序必须使用 database、memcached、dynamodb 或 redis 缓存驱动程序作为应用程序的默认缓存驱动程序。此外,所有服务器都必须与同一个中央缓存服务器进行通信。
如果你的应用程序调度程序运行在多台服务器上,你可以将定时任务限制为仅在单台服务器上执行。例如,假设你有一个每周五晚上生成新报告的定时任务。如果任务调度程序运行在三台工作服务器上,定时任务将在所有三台服务器上运行并生成三次报告。这不好!
要表明任务应仅在一台服务器上运行,请在定义定时任务时使用 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('https://laravel.net.cn'))2 ->name('check_uptime:laravel.com')3 ->everyFiveMinutes()4 ->onOneServer();5 6Schedule::job(new CheckUptime('https://vapor.laravel.net.cn'))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 方法仅在通过 command 和 exec 方法调度任务时才能使用。
维护模式
当应用程序处于 维护模式 时,应用程序的定时任务将不会运行,因为我们不希望任务干扰你在服务器上可能执行的任何未完成的维护工作。然而,如果你想强制任务即使在维护模式下也运行,你可以在定义任务时调用 evenInMaintenanceMode 方法。
1Schedule::command('emails:send')->evenInMaintenanceMode();
暂停调度任务
你可以使用 schedule:pause Artisan 命令暂时暂停定时任务处理,而无需更改已部署的代码。
1php artisan schedule:pause
当调度程序暂停时,定时任务不会运行。你可以使用 schedule:continue 命令恢复定时任务处理。
1php artisan schedule:continue
如果某个任务在调度程序暂停时仍需运行,你可以使用 evenWhenPaused 方法对其进行标记。
1Schedule::command('emails:send')->evenWhenPaused();
调度分组
当定义多个具有相似配置的定时任务时,你可以使用 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 });
运行调度程序
现在我们已经了解了如何定义定时任务,让我们讨论如何实际在服务器上运行它们。schedule:run Artisan 命令将评估你所有的定时任务,并根据服务器的当前时间确定它们是否需要运行。
因此,使用 Laravel 调度程序时,我们只需要在服务器上添加一个每分钟运行 schedule:run 命令的 cron 配置条目。如果你不知道如何向服务器添加 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:work Artisan 命令。此命令将在前台运行,并每分钟调用一次调度程序,直到你终止该命令。当定义了秒级任务时,调度程序将继续在每分钟内运行以处理这些任务。
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)
如果你只想在定时 Artisan 或系统命令以非零退出代码终止时发送输出邮件,请使用 emailOutputOnFailure 方法。
1Schedule::command('report:generate')2 ->daily()
emailOutputTo、emailOutputOnFailure、sendOutputTo 和 appendOutputTo 方法仅适用于 command 和 exec 方法。
任务钩子
使用 before 和 after 方法,你可以指定在定时任务执行前后执行的代码。
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 });
如果你的命令有可用输出,你可以在 after、onSuccess 或 onFailure 钩子中通过将 Illuminate\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
使用 pingBefore 和 thenPing 方法,调度程序可以在任务执行前或执行后自动 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、thenPingIf、pingOnSuccessIf 和 pingOnFailureIf 方法可用于仅在给定条件为 true 时 ping 给定的 URL。
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);
活动
Laravel 在调度过程中会分发各种 事件。你可以为以下任何事件 定义监听器:
| 事件名称 |
|---|
Illuminate\Console\Events\ScheduledTaskStarting |
Illuminate\Console\Events\ScheduledTaskFinished |
Illuminate\Console\Events\ScheduledBackgroundTaskFinished |
Illuminate\Console\Events\ScheduledTaskSkipped |
Illuminate\Console\Events\ScheduledTaskFailed |