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

调度频率选项

我们已经看到了一些如何配置任务以指定间隔运行的示例。但是,你可以分配给任务的调度频率还有很多:

方法描述
->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');

以下是额外的调度约束列表:

方法描述
->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 钩子中通过将 Illuminate\Support\Stringable 实例作为钩子闭包定义的 $output 参数进行类型提示来访问它:

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 在调度过程中分发各种事件。你可以为以下任何事件定义监听器

事件名称
Illuminate\Console\Events\ScheduledTaskStarting
Illuminate\Console\Events\ScheduledTaskFinished
Illuminate\Console\Events\ScheduledBackgroundTaskFinished
Illuminate\Console\Events\ScheduledTaskSkipped
Illuminate\Console\Events\ScheduledTaskFailed