任务调度
简介
过去,你可能为服务器上需要调度的每个任务都编写一条 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();除了使用闭包调度,还可调度可调用对象。可调用对象是包含 __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('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();调度频率选项
我们已经看过一些将任务配置为按指定间隔运行的示例。不过,还可为任务指定更多调度频率:
| Method | Description |
|---|---|
->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(); | 每月第一天 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');更多调度约束列表如下:
| Method | Description |
|---|---|
->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' => '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
要使用此功能,应用必须将 database、memcached、dynamodb 或 redis 缓存驱动作为默认缓存驱动。此外,所有服务器必须与同一中央缓存服务器通信。
若应用的调度器在多台服务器上运行,可将定时任务限制为仅在单台服务器上执行。例如,假设有一项每周五晚上生成新报告的定时任务。若任务调度器在三台工作服务器上运行,该任务会在三台服务器上都执行,从而生成三份报告。这可不妙!
要表明任务应仅在一台服务器上运行,在定义定时任务时使用 onOneServer 方法。最先获取到该任务的服务器会对任务加原子锁,防止其他服务器同时运行同一任务:
use Illuminate\Support\Facades\Schedule;
Schedule::command('report:generate')
->fridays()
->at('17:00')
->onOneServer();可使用 useCache 方法自定义调度器为单服务器任务获取原子锁时所使用的缓存存储:
Schedule::useCache('database');为单服务器任务命名
有时你可能需要用不同参数调度同一任务,同时仍指示 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();WARNING
runInBackground 方法仅可在通过 command 与 exec 方法调度任务时使用。
维护模式
应用处于维护模式时,定时任务不会运行,因为我们不希望任务干扰你可能正在服务器上进行的未完成维护。不过,若希望即使在维护模式下也强制运行某任务,可在定义任务时调用 evenInMaintenanceMode 方法:
Schedule::command('emails:send')->evenInMaintenanceMode();暂停定时任务
可使用 schedule:pause Artisan 命令,在不更改已部署代码的情况下临时暂停定时任务处理:
php artisan schedule:pause调度器暂停期间,不会运行任何定时任务。可使用 schedule:continue 命令恢复定时任务处理:
php artisan schedule:continue若某任务在调度器暂停时仍应运行,可用 evenWhenPaused 方法标记它:
Schedule::command('emails:send')->evenWhenPaused();调度分组
在定义多个配置相似的定时任务时,可使用 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 调度器时,只需在服务器上添加一条每分钟运行 schedule:run 命令的 cron 配置。若不知道如何向服务器添加 cron 条目,可考虑使用 Laravel Cloud 等托管平台,由它代为管理定时任务执行:
* * * * * 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');WARNING
emailOutputTo、emailOutputOnFailure、sendOutputTo 与 appendOutputTo 方法仅适用于 command 与 exec 方法。
任务钩子
使用 before 与 after 方法,可指定在定时任务执行前后要运行的代码:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')
->daily()
->before(function () {
// The task is about to execute...
})
->after(function () {
// The task has executed...
});onSuccess 与 onFailure 方法允许你指定在定时任务成功或失败时要执行的代码。失败表示定时 Artisan 或系统命令以非零退出码结束:
Schedule::command('emails:send')
->daily()
->onSuccess(function () {
// The task succeeded...
})
->onFailure(function () {
// The task failed...
});若命令有可用输出,可在 after、onSuccess 或 onFailure 钩子中,通过将钩子闭包的 $output 参数类型提示为 Illuminate\Support\Stringable 实例来访问它:
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
使用 pingBefore 与 thenPing 方法,调度器可在任务执行前后自动 ping 给定 URL。这可用于通知外部服务(如 Envoyer)定时任务即将开始或已经执行完毕:
Schedule::command('emails:send')
->daily()
->pingBefore($url)
->thenPing($url);可使用 pingOnSuccess 与 pingOnFailure 方法,仅在任务成功或失败时 ping 给定 URL。失败表示定时 Artisan 或系统命令以非零退出码结束:
Schedule::command('emails:send')
->daily()
->pingOnSuccess($successUrl)
->pingOnFailure($failureUrl);可使用 pingBeforeIf、thenPingIf、pingOnSuccessIf 与 pingOnFailureIf 方法,仅在给定条件为 true 时 ping 给定 URL:
Schedule::command('emails:send')
->daily()
->pingBeforeIf($condition, $url)
->thenPingIf($condition, $url);
Schedule::command('emails:send')
->daily()
->pingOnSuccessIf($condition, $successUrl)
->pingOnFailureIf($condition, $failureUrl);事件
| Event Name |
|---|
Illuminate\Console\Events\ScheduledTaskStarting |
Illuminate\Console\Events\ScheduledTaskFinished |
Illuminate\Console\Events\ScheduledBackgroundTaskFinished |
Illuminate\Console\Events\ScheduledTaskSkipped |
Illuminate\Console\Events\ScheduledTaskFailed |