Skip to content
全部文档

任务调度

简介

过去,你可能为服务器上需要调度的每个任务都编写一条 cron 配置。但这很快就会令人头疼:任务计划不再处于版本控制中,而且必须 SSH 到服务器才能查看现有 cron 条目或添加新条目。

Laravel 的命令调度器为在服务器上管理定时任务提供了全新方式。调度器允许你在 Laravel 应用内部流畅、富有表现力地定义命令计划。使用调度器时,服务器上只需一条 cron 条目。任务计划通常定义在应用的 routes/console.php 文件中。

定义调度

可在应用的 routes/console.php 文件中定义所有定时任务。我们先看一个示例:每天午夜调用一个闭包,在闭包中执行数据库查询以清空一张表:

php
<?php

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;

Schedule::call(function () {
    DB::table('recent_users')->delete();
})->daily();

除了使用闭包调度,还可调度可调用对象。可调用对象是包含 __invoke 方法的简单 PHP 类:

php
Schedule::call(new DeleteRecentUsers)->daily();

若希望将 routes/console.php 仅用于命令定义,可在应用的 bootstrap/app.php 文件中使用 withSchedule 方法定义定时任务。该方法接受一个接收调度器实例的闭包:

php
use Illuminate\Console\Scheduling\Schedule;

->withSchedule(function (Schedule $schedule) {
    $schedule->call(new DeleteRecentUsers)->daily();
})

若要查看定时任务概览及其下次计划运行时间,可使用 schedule:list Artisan 命令:

shell
php artisan schedule:list

调度 Artisan 命令

除了调度闭包,还可调度 Artisan 命令 与系统命令。例如,可使用 command 方法,通过命令名称或类来调度 Artisan 命令。

使用命令类名调度 Artisan 命令时,可传入在调用时应提供给命令的额外命令行参数数组:

php
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 命令,可在命令定义后链式调用调度相关方法:

php
Artisan::command('delete:recent-users', function () {
    DB::table('recent_users')->delete();
})->purpose('Delete recent users')->daily();

若需要向闭包命令传递参数,可将其提供给 schedule 方法:

php
Artisan::command('emails:send {user} {--force}', function ($user) {
    // ...
})->purpose('Send emails to the specified user')->schedule(['Taylor', '--force'])->daily();

调度队列任务

可使用 job 方法调度队列任务。该方法提供了一种便捷方式来调度队列任务,而无需使用 call 方法定义用于入队的闭包:

php
use App\Jobs\Heartbeat;
use Illuminate\Support\Facades\Schedule;

Schedule::job(new Heartbeat)->everyFiveMinutes();

可为 job 方法提供可选的第二、第三个参数,分别指定用于入队的队列名称与队列连接:

php
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 方法向操作系统发出命令:

php
use Illuminate\Support\Facades\Schedule;

Schedule::exec('node /home/forge/script.js')->daily();

调度频率选项

我们已经看过一些将任务配置为按指定间隔运行的示例。不过,还可为任务指定更多调度频率:

MethodDescription
->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');为任务设置时区。

这些方法可与额外约束组合,创建仅在一周中特定日期运行的更精细计划。例如,可调度命令每周一运行:

php
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');

更多调度约束列表如下:

MethodDescription
->weekdays();将任务限制在工作日。
->weekends();将任务限制在周末。
->sundays();将任务限制在周日。
->mondays();将任务限制在周一。
->tuesdays();将任务限制在周二。
->wednesdays();将任务限制在周三。
->thursdays();将任务限制在周四。
->fridays();将任务限制在周五。
->saturdays();将任务限制在周六。
->days(array|mixed);将任务限制在特定日期。
->between($startTime, $endTime);将任务限制在起止时间之间运行。
->unlessBetween($startTime, $endTime);将任务限制为不在起止时间之间运行。
->when(Closure);基于真值测试限制任务。
->environments($env);将任务限制在特定环境。

日期约束

可使用 days 方法将任务执行限制在一周中的特定日期。例如,可调度命令在周日与周三每小时运行:

php
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')
    ->hourly()
    ->days([0, 3]);

或者,在定义任务应运行的日期时,可使用 Illuminate\Console\Scheduling\Schedule 类上可用的常量:

php
use Illuminate\Support\Facades;
use Illuminate\Console\Scheduling\Schedule;

Facades\Schedule::command('emails:send')
    ->hourly()
    ->days([Schedule::SUNDAY, Schedule::WEDNESDAY]);

时间区间约束

可使用 between 方法根据一天中的时间限制任务执行:

php
Schedule::command('emails:send')
    ->hourly()
    ->between('7:00', '22:00');

类似地,可使用 unlessBetween 方法在某段时间内排除任务执行:

php
Schedule::command('emails:send')
    ->hourly()
    ->unlessBetween('23:00', '4:00');

真值测试约束

可使用 when 方法根据给定真值测试的结果限制任务执行。换言之,若给定闭包返回 true,只要没有其他约束阻止运行,任务就会执行:

php
Schedule::command('emails:send')->daily()->when(function () {
    return true;
});

skip 方法可视为 when 的反面。若 skip 返回 true,定时任务将不会执行:

php
Schedule::command('emails:send')->daily()->skip(function () {
    return true;
});

使用链式 when 方法时,仅当所有 when 条件都返回 true 时,定时命令才会执行。

环境约束

可使用 environments 方法仅在给定环境(由 APP_ENV 环境变量 定义)中执行任务:

php
Schedule::command('emails:send')
    ->daily()
    ->environments(['staging', 'production']);

时区

使用 timezone 方法,可指定定时任务的时间应按给定时区解释:

php
use Illuminate\Support\Facades\Schedule;

Schedule::command('report:generate')
    ->timezone('America/New_York')
    ->at('2:00')

若反复为所有定时任务指定相同的时区,可在应用的 app 配置文件中定义 schedule_timezone 选项,为所有调度指定时区:

php
'timezone' => 'UTC',

'schedule_timezone' => 'America/Chicago',

WARNING

请注意,某些时区使用夏令时。夏令时切换时,定时任务可能运行两次,甚至完全不运行。因此我们建议尽可能避免基于时区的调度。

防止任务重叠

默认情况下,即使任务的上一次实例仍在运行,定时任务也会继续执行。要防止这种情况,可使用 withoutOverlapping 方法:

php
use Illuminate\Support\Facades\Schedule;

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

在此例中,若 emails:send Artisan 命令 尚未在运行,则会每分钟运行一次。若任务执行时间差异很大、难以准确预估耗时,withoutOverlapping 方法尤其有用。

如有需要,可指定「防止重叠」锁在多少分钟后过期。默认情况下,锁会在 24 小时后过期:

php
Schedule::command('emails:send')->withoutOverlapping(10);

在底层,withoutOverlapping 方法使用应用的缓存获取锁。如有必要,可使用 schedule:clear-cache Artisan 命令清除这些缓存锁。通常仅在因意外服务器问题导致任务卡住时才需要这样做。

在单台服务器上运行任务

WARNING

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

若应用的调度器在多台服务器上运行,可将定时任务限制为仅在单台服务器上执行。例如,假设有一项每周五晚上生成新报告的定时任务。若任务调度器在三台工作服务器上运行,该任务会在三台服务器上都执行,从而生成三份报告。这可不妙!

要表明任务应仅在一台服务器上运行,在定义定时任务时使用 onOneServer 方法。最先获取到该任务的服务器会对任务加原子锁,防止其他服务器同时运行同一任务:

php
use Illuminate\Support\Facades\Schedule;

Schedule::command('report:generate')
    ->fridays()
    ->at('17:00')
    ->onOneServer();

可使用 useCache 方法自定义调度器为单服务器任务获取原子锁时所使用的缓存存储:

php
Schedule::useCache('database');

为单服务器任务命名

有时你可能需要用不同参数调度同一任务,同时仍指示 Laravel 让每种参数组合仅在单台服务器上运行。为此,可通过 name 方法为每个调度定义分配唯一名称:

php
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();

同样,若希望闭包任务在单台服务器上运行,也必须为其指定名称:

php
Schedule::call(fn () => User::resetApiRequestCount())
    ->name('reset-api-request-count')
    ->daily()
    ->onOneServer();

后台任务

默认情况下,在同一时间调度的多个任务会按在 schedule 方法中定义的顺序依次执行。若有长时间运行的任务,可能导致后续任务启动远晚于预期。若希望在后台运行任务以便它们同时执行,可使用 runInBackground 方法:

php
use Illuminate\Support\Facades\Schedule;

Schedule::command('analytics:report')
    ->daily()
    ->runInBackground();

WARNING

runInBackground 方法仅可在通过 commandexec 方法调度任务时使用。

维护模式

应用处于维护模式时,定时任务不会运行,因为我们不希望任务干扰你可能正在服务器上进行的未完成维护。不过,若希望即使在维护模式下也强制运行某任务,可在定义任务时调用 evenInMaintenanceMode 方法:

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

暂停定时任务

可使用 schedule:pause Artisan 命令,在不更改已部署代码的情况下临时暂停定时任务处理:

shell
php artisan schedule:pause

调度器暂停期间,不会运行任何定时任务。可使用 schedule:continue 命令恢复定时任务处理:

shell
php artisan schedule:continue

若某任务在调度器暂停时仍应运行,可用 evenWhenPaused 方法标记它:

php
Schedule::command('emails:send')->evenWhenPaused();

调度分组

在定义多个配置相似的定时任务时,可使用 Laravel 的任务分组功能,避免为每个任务重复相同设置。分组可简化代码,并确保相关任务配置一致。

要创建一组定时任务,先调用所需的任务配置方法,再调用 group 方法。group 接受一个闭包,用于定义共享指定配置的任务:

php
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 等托管平台,由它代为管理定时任务执行:

shell
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

亚分钟定时任务

在大多数操作系统上,cron 任务最多每分钟运行一次。不过,Laravel 的调度器允许以更频繁的间隔调度任务,甚至可达每秒一次:

php
use Illuminate\Support\Facades\Schedule;

Schedule::call(function () {
    DB::table('recent_users')->delete();
})->everySecond();

当应用中定义了亚分钟任务时,schedule:run 命令会持续运行到当前分钟结束,而不是立即退出。这样命令可在整分钟内调用所有需要的亚分钟任务。

由于耗时超出预期的亚分钟任务可能延迟后续亚分钟任务的执行,建议所有亚分钟任务都派发队列任务或后台命令来处理实际工作:

php
use App\Jobs\DeleteRecentUsers;

Schedule::job(new DeleteRecentUsers)->everyTenSeconds();

Schedule::command('users:delete')->everyTenSeconds()->runInBackground();

中断亚分钟任务

由于定义了亚分钟任务时,schedule:run 命令会在整分钟内持续运行,部署应用时有时需要中断该命令。否则,已在运行的 schedule:run 实例会继续使用应用先前部署的代码,直到当前分钟结束。

要中断正在进行的 schedule:run 调用,可将 schedule:interrupt 命令加入应用的部署脚本。该命令应在应用部署完成之后调用:

shell
php artisan schedule:interrupt

在本地运行调度器

通常不会在本地开发机器上添加调度器 cron 条目。相反,可使用 schedule:work Artisan 命令。该命令会在前台运行,并每分钟调用一次调度器,直到你终止命令。若定义了亚分钟任务,调度器会在每分钟内继续运行以处理这些任务:

shell
php artisan schedule:work

任务输出

Laravel 调度器提供了多种处理定时任务输出的便捷方法。首先,使用 sendOutputTo 方法可将输出发送到文件以便稍后检查:

php
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')
    ->daily()
    ->sendOutputTo($filePath);

若希望将输出追加到给定文件,可使用 appendOutputTo 方法:

php
Schedule::command('emails:send')
    ->daily()
    ->appendOutputTo($filePath);

使用 emailOutputTo 方法,可将输出通过邮件发送到你选择的地址。在邮件发送任务输出之前,应先配置 Laravel 的邮件服务

php
Schedule::command('report:generate')
    ->daily()
    ->sendOutputTo($filePath)
    ->emailOutputTo('taylor@example.com');

若仅希望在定时 Artisan 或系统命令以非零退出码结束时才邮件发送输出,可使用 emailOutputOnFailure 方法:

php
Schedule::command('report:generate')
    ->daily()
    ->emailOutputOnFailure('taylor@example.com');

WARNING

emailOutputToemailOutputOnFailuresendOutputToappendOutputTo 方法仅适用于 commandexec 方法。

任务钩子

使用 beforeafter 方法,可指定在定时任务执行前后要运行的代码:

php
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 或系统命令以非零退出码结束:

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

若命令有可用输出,可在 afteronSuccessonFailure 钩子中,通过将钩子闭包的 $output 参数类型提示为 Illuminate\Support\Stringable 实例来访问它:

php
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)定时任务即将开始或已经执行完毕:

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

可使用 pingOnSuccesspingOnFailure 方法,仅在任务成功或失败时 ping 给定 URL。失败表示定时 Artisan 或系统命令以非零退出码结束:

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

可使用 pingBeforeIfthenPingIfpingOnSuccessIfpingOnFailureIf 方法,仅在给定条件为 true 时 ping 给定 URL:

php
Schedule::command('emails:send')
    ->daily()
    ->pingBeforeIf($condition, $url)
    ->thenPingIf($condition, $url);

Schedule::command('emails:send')
    ->daily()
    ->pingOnSuccessIf($condition, $successUrl)
    ->pingOnFailureIf($condition, $failureUrl);

事件

Laravel 在调度过程中会派发多种事件。你可为下列任意事件定义监听器

Event Name
Illuminate\Console\Events\ScheduledTaskStarting
Illuminate\Console\Events\ScheduledTaskFinished
Illuminate\Console\Events\ScheduledBackgroundTaskFinished
Illuminate\Console\Events\ScheduledTaskSkipped
Illuminate\Console\Events\ScheduledTaskFailed