任务调度
任务调度
简介
过去,你可能需要在服务器上为每一个调度任务去创建一个 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();
除了使用闭包调度外,还可以调度 invokable 对象。invokable 对象指的是包含 __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;
// 将任务分派到 「sqs」连接上的「心跳」队列...
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); |
每小时整点过十七分运行一次任务 |
->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;
// 每周运行一次,时间为周一下午 1 点...
Schedule::call(function () {
// ...
})->weekly()->mondays()->at('13:00');
// 在工作日上午 8 点至下午 5 点之间,每小时运行一次...
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' => env('APP_TIMEZONE', 'UTC'),
'schedule_timezone' => 'America/Chicago',
[!WARNING]
请记住,有些时区使用夏令时。当夏令时发生变化时,你的计划任务可能会运行两次,甚至根本不会运行。因此,我们建议尽可能避免时区调度。
避免任务重复
默认情况下,即使之前的任务实例还在执行,计划内的任务也会执行。为避免这种情况的发生,你可以使用 withoutOverlapping
方法:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')->withoutOverlapping();
在此例中,若 emails:send
Artisan 命令 尚未运行,则它将会每分钟执行一次。如果任务的执行时间非常不确定,导致你无法准确预测任务的执行时间,那么 withoutOverlapping
方法就特别有用。
如有需要,你可以在 withoutOverlapping
锁过期之前,指定它的过期分钟数。默认情况下,这个锁会在 24 小时后过期:
Schedule::command('emails:send')->withoutOverlapping(10);
在幕后,withoutOverlapping 方法使用应用程序的 cache 来获取锁。如果必要,你可以使用 schedule:clear-cache
Artisan 命令清除这些缓存锁。通常只有在服务器出现意外问题导致任务卡住时才需要这样做。
任务只运行在一台服务器上
[!WARNING]
要使用此功能,你的应用程序必须使用database
、memcached
、dynamodb
或redis
缓存驱动程序作为应用程序的默认缓存驱动程序。此外,所有服务器都必须与同一个中央缓存服务器通信。
如果你的应用程序的调度器在多台服务器上运行,你可以限制计划任务只在一台服务器上执行。例如,假设你有一个计划任务,每周五晚上生成一份新报告。如果任务调度器在三个工作服务器上运行,则计划任务将在所有三个服务器上运行并三次生成报告。这不好!
要表示任务仅在一台服务器上运行,请在定义计划任务时使用 onOneServer
方法。第一个获得任务的服务器将确保任务的原子锁,以防止其他服务器同时运行同一任务:
use Illuminate\Support\Facades\Schedule;
Schedule::command('report:generate')
->fridays()
->at('17:00')
->onOneServer();
为单服务器任务命名
有时,你可能需要使用不同的参数调度相同的作业,同时指示 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:run
Artisan 命令将评估你的所有计划任务,并根据服务器的当前时间决定它们是否运行。
因此,当使用 Laravel 的调度器时,我们只需要在服务器上添加一个单一的 cron 配置项,每分钟运行一次 schedule:run
命令。如果你不知道如何向服务器添加 cron 条目,请考虑使用诸如 Laravel Forge 之类的服务来为你管理 cron 配置项:
* * * * * 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('[email protected]');
如果你只想在预定的 Artisan 或系统命令以非零退出代码终止时通过电子邮件发送输出,请使用 emailOutputOnFailure
方法:
Schedule::command('report:generate')
->daily()
->emailOutputOnFailure('[email protected]');
[!WARNING]
emailOutputTo
,emailOutputOnFailure
,sendOutputTo
和appendOutputTo
方法是command
和exec
方法所独有的。
任务钩子
使用 before
和 after
方法,你可以指定在计划任务执行前后要执行的代码:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')
->daily()
->before(function () {
// 任务即将执行...
})
->after(function () {
// 任务已执行...
});
onSuccess
和 onFailure
方法允许你指定在计划任务成功或失败时执行的代码。失败表示预定的 Artisan 或系统命令以非零的退出代码终止:
Schedule::command('emails:send')
->daily()
->onSuccess(function () {
// 任务成功...
})
->onFailure(function () {
// 任务失败...
});
如果命令中提供了输出,你可以在 after
、onSuccess
或 onFailure
钩子中访问它,方法是将 Illuminate\Support\Stringable
实例作为钩子闭包定义的 $output
参数进行类型提示:
use Illuminate\Support\Stringable;
Schedule::command('emails:send')
->daily()
->onSuccess(function (Stringable $output) {
// 任务成功...
})
->onFailure(function (Stringable $output) {
// 任务失败...
});
Ping 网址
使用 pingBefore
和 thenPing
方法,调度器可以在任务执行前后自动 ping 给定的网址。此方法对于通知你计划任务正在开始或已完成执行的外部服务(如 Envoyer)非常有用:
Schedule::command('emails:send')
->daily()
->pingBefore($url)
->thenPing($url);
pingBeforeIf
和 thenPingIf
方法可用于仅在给定条件为 true
时 ping 给定网址:
Schedule::command('emails:send')
->daily()
->pingBeforeIf($condition, $url)
->thenPingIf($condition, $url);
pingOnSuccess
和 pingOnFailure
方法可用于仅在任务成功或失败时 ping 给定的网址。失败表示预定的 Artisan 或系统命令以非零退出代码终止:
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 |
原文地址:cndocs/11.x/sc...
译文地址:cndocs/11.x/sc...