任务调度
介绍
过去,您可能为每个需要在服务器上调度的任务编写了一个 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('删除最近的用户')->daily();
如果您需要向闭包命令传递参数,可以将它们提供给 schedule
方法:
Artisan::command('emails:send {user} {--force}', function ($user) {
// ...
})->purpose('向指定用户发送电子邮件')->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;
// 将作业调度到 "heartbeats" 队列,使用 "sqs" 连接...
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;
// 每周一下午1点运行一次...
Schedule::call(function () {
// ...
})->weekly()->mondays()->at('13:00');
// 在工作日的早上8点到下午5点每小时运行...
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',
请记住,一些时区会使用夏令时。当夏令时变化发生时,您的调度任务可能会运行两次,或者根本不运行。因此,我们建议在可能的情况下避免时区调度。
防止任务重叠
默认情况下,调度任务将运行,即使前一个任务实例仍在运行。要防止这种情况,您可以使用 withoutOverlapping
方法:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')->withoutOverlapping();
在此示例中,emails:send
Artisan 命令 将在每分钟运行,如果它尚未运行。withoutOverlapping
方法在您有执行时间变化很大的任务时尤其有用,防止您无法预测给定任务将花费多长时间。
如果需要,您可以指定在“无重叠”锁过期之前必须经过多少分钟。默认情况下,锁将在 24 小时后过期:
Schedule::command('emails:send')->withoutOverlapping(10);
在后台,withoutOverlapping
方法利用您的应用程序的 cache 来获取锁。如果需要,您可以使用 schedule:clear-cache
Artisan 命令清除这些缓存锁。这通常只有在任务由于意外的服务器问题而卡住时才需要。
在一台服务器上运行任务
要利用此功能,您的应用程序必须使用 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();
runInBackground
方法仅在通过 command
和 exec
方法调度任务时可用。
维护模式
当应用程序处于 维护模式 时,您的应用程序的调度任务将不会运行,因为我们不希望您的任务干扰您在服务器上执行的任何未完成的维护。但是,如果您希望强制任务在维护模式下运行,可以在定义任务时调用 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 的调度器时,我们只需要在服务器上添加一个 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('taylor@example.com');
如果您只想在调度的 Artisan 或系统命令以非零退出代码终止时发送电子邮件输出,可以使用 emailOutputOnFailure
方法:
Schedule::command('report:generate')
->daily()
->emailOutputOnFailure('taylor@example.com');
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 () {
// 任务失败...
});
如果您的命令有输出可用,您可以通过在钩子闭包定义中类型提示 Illuminate\Support\Stringable
实例作为 $output
参数来访问它:
use Illuminate\Support\Stringable;
Schedule::command('emails:send')
->daily()
->onSuccess(function (Stringable $output) {
// 任务成功...
})
->onFailure(function (Stringable $output) {
// 任务失败...
});
发送 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);
事件
Laravel 在调度过程中触发多种 事件。您可以为以下任何事件 定义监听器:
事件名称 |
---|
Illuminate\Console\Events\ScheduledTaskStarting |
Illuminate\Console\Events\ScheduledTaskFinished |
Illuminate\Console\Events\ScheduledBackgroundTaskFinished |
Illuminate\Console\Events\ScheduledTaskSkipped |
Illuminate\Console\Events\ScheduledTaskFailed |