任务调度


介绍

过去,您可能需要为服务器上每个需要计划的任务编写一个 cron 配置条目。然而,这很快会变得非常麻烦,因为您的任务计划不再在源代码控制中,您必须通过 SSH 登录到服务器才能查看现有的 cron 条目或添加其他条目。

Laravel 的命令调度程序为在服务器上管理计划任务提供了一种全新的方法。调度程序允许您在 Laravel 应用程序中流畅而富有表现力地定义命令计划。使用调度程序时,您的服务器只需要一个 cron 条目。您的任务计划在app/Console/Kernel.php 文件的 schedule 方法中定义。为了帮助您入门,该方法中定义了一个简单的示例。

定义计划

您可以在应用程序的 App\Console\Kernel 类的 schedule 方法中定义所有的计划任务。让我们从一个示例开始。在此示例中,我们将调度一个闭包在每天午夜被调用。在闭包内,我们将执行一个数据库查询以清除一个表:

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\DB;

class Kernel extends ConsoleKernel
{
    /**
     * Define the application's command schedule.
     */
    protected function schedule(Schedule $schedule): void
    {
        $schedule->call(function () {
            DB::table('recent_users')->delete();
        })->daily();
    }
}

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

$schedule->call(new DeleteRecentUsers)->daily();

如果您想查看计划任务的概述以及它们下次运行的时间,可以使用 schedule:list Artisan命令:

php artisan schedule:list

调度 Artisan 命令

除了调度使用闭包的任务,您还可以调度 Artisan 命令和系统命令。例如,您可以使用 command 方法使用命令的名称或类调度 Artisan 命令。

使用命令的类名调度 Artisan 命令时,您可以传递额外的命令行参数数组,这些参数将在调用命令时提供:

use App\Console\Commands\SendEmailsCommand;

$schedule->command('emails:send Taylor --force')->daily();

$schedule->command(SendEmailsCommand::class, ['Taylor', '--force'])->daily();

调度队列作业

可以使用 job 方法安排队列作业。此方法提供了一种方便的方法来安排队列作业,而无需使用 call 方法来定义闭包来排队作业:

use App\Jobs\Heartbeat;

$schedule->job(new Heartbeat)->everyFiveMinutes();

job 方法可以提供第二个和第三个参数来指定应用于队列作业的队列名称和队列连接:

use App\Jobs\Heartbeat;

// 将作业调度到"sqs"连接上的"heartbeats"队列...
$schedule->job(new Heartbeat, 'heartbeats', 'sqs')->everyFiveMinutes();

调度 shell 命令

exec 方法可以用于向操作系统发送命令:

$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') 设置任务的时区

这些方法可以与其他约束相结合,以创建更精确的任务计划,只在一周的某些天运行。例如,您可以安排命令每周一运行一次:

// 每周一下午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 方法可以用于将任务执行限制为一周中的特定天。例如,您可以安排命令每小时在周日和周三运行:

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

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

use Illuminate\Console\Scheduling\Schedule;

$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 方法,您可以指定计划任务的时间应该在给定的时区内解释:

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

如果您为所有计划任务重复分配相同的时区,可以在 App\Console\Kernel 类中定义一个 scheduleTimezone 方法。该方法应该返回应分配给所有计划任务的默认时区:

use DateTimeZone;
 
/**
 * Get the timezone that should be used by default for scheduled events.
 */
protected function scheduleTimezone(): DateTimeZone|string|null
{
    return 'America/Chicago';
}

请记住,某些时区使用夏令时。当夏时制更改发生时,由于计划的任务可能会运行两次或甚至根本不运行,因此我们建议在可能的情况下避免使用时区调度。

防止任务重叠

默认情况下,即使任务的上一个实例仍在运行,计划的任务也会运行。为了防止这种情况发生,您可以使用withoutOverlapping 方法:

$schedule->command('emails:send')->withoutOverlapping();

在此示例中,如果 emails:send Artisan命令尚未运行,则每分钟运行一次。withoutOverlapping 方法在您的任务执行时间差异很大,并且无法预测给定任务需要多长时间时特别有用。

如果需要,您可以指定在“不重叠”锁定过期之前必须经过多少分钟。默认情况下,锁定在24小时后过期:

$schedule->command('emails:send')->withoutOverlapping(10);

在幕后,withoutOverlapping 方法使用应用程序的缓存来获取锁定。如果需要,您可以使用 schedule:clear-cache Artisan 命令清除这些缓存锁定。只有在任务由于意外的服务器问题而被卡住时才需要这样做。

在一个服务器上运行任务

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

如果应用程序的调度程序在多台服务器上运行,可以限制计划作业只在一台服务器上执行。例如,假设您有一个定期每周五晚生成新报告的调度任务。如果任务调度程序在三台工作服务器上运行,则计划的任务将在所有三台服务器上运行并生成三次报告。非常糟糕!

为了指示任务只在一个服务器上运行,请在定义计划任务时使用 onOneServer 方法。首先获取任务的服务器将在作业上获得原子锁,以防止其他服务器同时运行相同的任务:

$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 方法:

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

只有在使用 commandexec 方法安排任务时,才可以使用 runInBackground 方法。

维护模式

在应用程序处于维护模式时,不会运行应用程序的计划任务,因为我们不希望这些任务干扰您在服务器上进行的未完成的维护工作。但是,如果要在维护模式下强制运行任务,可以在定义任务时调用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 的调度程序允许您安排任务以更频繁的间隔运行,甚至可以每秒钟运行一次:

$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 方法,您可以将输出发送到文件以供以后检查:

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

emailOutputToemailOutputOnFailuresendOutputToappendOutputTo 方法专门用于 commandexec 方法。

任务挂钩

使用 beforeafter 方法,您可以指定在执行计划任务之前和之后要执行的代码:

$schedule->command('emails:send')
         ->daily()
         ->before(function () {
             // 任务即将执行...
         })
         ->after(function () {
             // 任务已执行...
         });

onSuccessonFailure 方法允许您指定在计划的任务成功或失败时执行的代码。失败表示计划的 Artisan 或系统命令以非零退出代码终止:

$schedule->command('emails:send')
         ->daily()
         ->onSuccess(function () {
             // 任务成功...
         })
         ->onFailure(function () {
             // 任务失败...
         });

如果从命令获取输出,则可以在 afteronSuccessonFailure 挂钩中通过将 Illuminate\Support\Stringable 实例作为钩子闭包定义的 $output 参数进行访问:

use Illuminate\Support\Stringable;

$schedule->command('emails:send')
         ->daily()
         ->onSuccess(function (Stringable $output) {
             // 任务成功...
         })
         ->onFailure(function (Stringable $output) {
             // 任务失败...
         });

ping URL

使用 pingBeforethenPing 方法,调度程序可以在任务执行前或执行后自动 ping 给定的 URL。此方法可用于通知外部服务(例如 Envoyer),您的计划任务正在开始或已完成执行:

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

pingBeforeIfthenPingIf 方法可以仅在给定条件为 true 时 ping 给定的 URL:

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

pingOnSuccesspingOnFailure 方法可以仅在任务成功或失败时 ping 给定的 URL。失败表示计划的 Artisan 或系统命令以非零退出代码终止:

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

所有的 ping 方法都需要 Guzzle HTTP 库。Guzzle 通常在所有新的 Laravel 项目中默认安装,但是,如果不小心删除了 Guzzle,则可以使用 Composer 软件包管理器手动将 Guzzle 安装到您的项目中:

composer require guzzlehttp/guzzle

事件

如果需要,您可以侦听调度程序发送的事件。通常,事件侦听器映射将在应用程序的App\Providers\EventServiceProvider 类中定义:

/**
 * The event listener mappings for the application.
 *
 * @var array
 */
protected $listen = [
    'Illuminate\Console\Events\ScheduledTaskStarting' => [
        'App\Listeners\LogScheduledTaskStarting',
    ],
 
    'Illuminate\Console\Events\ScheduledTaskFinished' => [
        'App\Listeners\LogScheduledTaskFinished',
    ],
 
    'Illuminate\Console\Events\ScheduledBackgroundTaskFinished' => [
        'App\Listeners\LogScheduledBackgroundTaskFinished',
    ],
 
    'Illuminate\Console\Events\ScheduledTaskSkipped' => [
        'App\Listeners\LogScheduledTaskSkipped',
    ],
 
    'Illuminate\Console\Events\ScheduledTaskFailed' => [
        'App\Listeners\LogScheduledTaskFailed',
    ],
];

点赞 取消点赞 收藏 取消收藏

<< 上一篇: 频率限制

>> 下一篇: 认证