Laravel-Schedule 计划任务「原理了解」

Laravel-Schedule 原理了解

文章概述

Laravel-Schedule 流程分为两个步骤:

  • 第一步,根据配置的 Command 命令、Cron 表达式进行注册事件;
  • 第二步,操作系统配置每分钟触发Laravel-Schedule,由Laravel-Schedule自主完成事件是否符合执行时间过滤重复性检查,并可选Background或者Foreground进行执行任务。

事件注册

首先,在命令行应用程序入口文件 artisan 引入 bootstrap/app.php

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

然后,向容器中注册Laravel-Kernel,并使用make构建实例

$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);

App\Console\Kernel 继承于 Illuminate\Foundation\Console\Kernel

所以在实例过程中会调用Illuminate\Foundation\Console\Kernel构造方法:

public function __construct(Application $app, Dispatcher $events)
{
    if (! defined('ARTISAN_BINARY')) {
        define('ARTISAN_BINARY', 'artisan');
    }
    $this->app = $app;
    $this->events = $events;
    $this->app->booted(function () {
        $this->defineConsoleSchedule();
    });
}

这里又完成了一次事件注册,在应用启动booted完成后回调 $this->defineConsoleSchedule()

protected function defineConsoleSchedule()
{
    $this->app->singleton(Schedule::class, function ($app) {
        return new Schedule;
    });
    $schedule = $this->app->make(Schedule::class);
    $this->schedule($schedule);
}

重点在于defineConsoleSchedule这个方法,容器中注册并实例化Schedule对象,并使用址传递对Schedule实例进行操作,这里的操作就是计划任务的事件注册

Illuminate\Foundation\Console\Kernel 中的schedule方法

/**
  * Define the application's command schedule.
  *
  * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
  * @return void
  */
protected function schedule(Schedule $schedule)
{
    //
}

当然,我们不需要在这里修改任何代码。上面,我们说过Laravel-Kernel对象的实例类是App\Console\Kernel,他继承了Illuminate\Foundation\Console\Kernel类。

所以我们在官方文档中也可以清楚看到,计划任务的配置是在App\Console\Kernel中的schedule方法中定义的,例如:

/**
  * Define the application's command schedule.
  *
  * @param  \Illuminate\Console\Scheduling\Schedule $schedule
  * @return void
  */
protected function schedule(Schedule $schedule)
{
    $schedule->command('inspire')->hourly();
}

我们来看看官方文档的解读:

Closure 定义调度

使用Closure定义调度。例如,每天使用DB构造器方式来清空数据库一个表

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

Artisan 命令调度

除了计划 Closure 调用,你还能调度 Artisan 命令 和操作系统命令。举个例子,你可以给 command 方法传递命令名称或者类名称来调度一个 Artisan 命令:

$schedule->command('emaiLaravel-Schedule:send --force')->daily();

队列任务调度

job方法可以用来调度 队列任务。这个方法提供了一种快捷方式来调度任务,无需使用call方法手动创建闭包来调度任务:

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

Shell 命令调度

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

$schedule->exec('node /home/forge/script.js')->daily();

Laravel-Schedule 提供很多提高我们开发效率的执行频率方法

方法 描述
->cron(' '); 在自定义的 Cron 时间表上执行该任务
->everyMinute(); 每分钟执行一次任务
->everyFiveMinutes(); 每五分钟执行一次任务
->everyTenMinutes(); 每十分钟执行一次任务
->everyFifteenMinutes(); 每十五分钟执行一次任务
->everyThirtyMinutes(); 每半小时执行一次任务
->hourly(); 每小时执行一次任务
->hourlyAt(17); 每小时的第 17 分钟执行一次任务
->daily(); 每天午夜执行一次任务
->dailyAt('13:00'); 每天的 13:00 执行一次任务
->twiceDaily(1, 13); 每天的 1:00 和 13:00 分别执行一次任务
->weekly(); 每周执行一次任务
->monthly(); 每月执行一次任务
->monthlyOn(4, '15:00'); 在每个月的第四天的 15:00 执行一次任务
->quarterly(); 每季度执行一次任务
->yearly(); 每年执行一次任务
->timezone('America/New_York'); 设置时区

了解Laravel-Schedule给我们提供的多种任务定义和执行频率设置的方式后,我们回来思考其实现的原理是怎么样的?

schedule方法里的每一句任务的定义,就是构造一个事件对象,并将这个事件对象放到集合数组里

Illuminate\Console\Scheduling\Schedule.php command方法的核心实现代码如下:

$this->events[] = $event = new Event($this->mutex, $command);

mutex 这个变量用来控制事件当前时间执行的不可重复性

schedule方法里的每一句调度频率设置,就是表达式的构建

这个表达式 expression 就是与我们常用 crontab 表达式是同样的类型,everyTenMinutes() 每十分钟执行一次,其实对应的表达式就是 */10 * * * * *,具体 Laravel-Schedule 实现代码如下,应该不难看懂。

public $expression = '* * * * * *';

public function everyTenMinutes()
{
    return $this->spliceIntoPosition(1, '*/10');
}
protected function spliceIntoPosition($position, $value)
{
    $segments = explode(' ', $this->expression);
    $segments[$position - 1] = $value;
    return $this->cron(implode(' ', $segments));
}

这个很重要,因为事件的过滤中,需要匹配执行时间是否等于当前时间。

运行事件

启动调度器,使用调度器时,只需将以下Cron项目添加到服务器:

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

上面这个Cron会每分钟调用一次Laravel-Schedule命令调度器。执行schedule:run命令时, Laravel-Schedule会根据你的调度运行预定任务。

让我们带着疑问继续理解Laravel-Schedule运行事件原理。

schedule:run 是什么?

我们看 Illuminate\Console\Scheduling\ScheduleRunCommand 代码是怎么写的?和普通自定义Artisan命令一样,继承 Command 基类。然后具体任务内容在handle方法里实现。

class ScheduleRunCommand extends Command
{
    //...
    public function handle()
    {
        foreach ($this->schedule->dueEvents($this->laravel) as $event) {
            // ...
            $event->run($this->laravel);
        }
        // ...
    }
}

dueEvents 完成过滤动作 collect($this->events)->filter->isDue($app) 使用 isDue 方法进行过滤。

public function isDue($app)
{
    if (! $this->runsInMaintenanceMode() && $app->isDownForMaintenance()) {
        return faLaravel-Schedulee;
    }
    return $this->expressionPasses() &&
            $this->runsInEnvironment($app->environment());
}
protected function expressionPasses()
{
    $date = Carbon::now();

    if ($this->timezone) {
        $date->setTimezone($this->timezone);
    }

    return CronExpression::factory($this->expression)->isDue($date->toDateTimeString());
}

其实原理很简单,方法 expressionPasses 通过 Carbon 第三方扩展包获取当前时间,并与Event实例的 Expression 进行匹对

return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime

如果返回True,那就表示Event需要执行

$event->run($this->laravel);
public function run(Container $container)
{
    if ($this->withoutOverlapping &&
        ! $this->mutex->create($this)) {
        return;
    }
    $this->runInBackground
                ? $this->runCommandInBackground($container)
                : $this->runCommandInForeground($container);
}

withoutOverlappingmutex 就是在这里控制任务重复执行

(new Process(
    $this->buildCommand(), base_path(), null, null, null
))->run();

最后,由执行器执行命令任务...done


几点疑问?

1.假设每个五分钟执行,比如08:52定义命令调度 CommandSchedule ,会在08:57时刻执行?

不会,只会在08:55时刻执行,也就是满足时钟的固定周期。

2.任务调度的两种执行方式 runCommandInBackgroundrunCommandInForeground 有什么区别?

runCommandInBackground 代码如下:

protected function runCommandInBackground(Container $container)
{
    $this->callBeforeCallbacks($container);

    (new Process(
        $this->buildCommand(), base_path(), null, null, null
    ))->run();
}

runCommandInForeground 代码如下:

 protected function runCommandInForeground(Container $container)
{
    $this->callBeforeCallbacks($container);

    (new Process(
        $this->buildCommand(), base_path(), null, null, null
    ))->run();

    $this->callAfterCallbacks($container);
}

差别在于 $this->callAfterCallbacks($container) ,是否等待当前任务执行完成,如果选择 runCommandInBackground 方式运行,任务命令直接传递给操作系统进行执行,然后直接返回,等待操作系统执行完成任务后,会执行另一条命令 schedule:finish 通过事件ID进行异步响应对应的任务事件。

3.Closure 定义调度,和命令其他方式定义调度是不相同的,详细可以查看CallBackEvent->run() 同步方式执行

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 2年前 自动加精
Sparkfly
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 5
longren610

windows系统中使用 runInBackground 方式执行任务无效,在日志中发现提示Could not open input file: artisan ,应该是任务发送给操作系统执行的时候,没有指定artisan 所在目录。但是看了配置,也没有配置artisan目录的地方。
下面是任务执行返回的日志,可以看到artisan 没有指定目录,导致执行失败:

Running scheduled command: ("D:\php-7.2.20-nts-Win32-VC15-x64\php.exe" "artisan" command:lost_daka >> "D:\web\storage\logs/schedule/data_2019083009.log" 2>&1 & "D:\php-7.2.20-nts-Win32-VC15-x64\php.exe" "artisan" schedule:finish "framework\schedule-f9f1dbafe24484df4042b5e3b7379d925da05d99") > "NUL" 2>&1 &

同样的任务,如果不使用 runInBackground 是能免正常执行的。求教博主,这种情况要怎么解决呢?

4年前 评论
QJAutumn 4年前
longren610 (作者) 4年前
DashingLi 2年前

真理往往就是一句话。

6个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!