laravel 定时任务源码简析

2,032 阅读2分钟

题目设置

有一个动态配置数组, 每个数组元素定时启动任务. 如何实现?

源码基于 laravel 5.5.45.

如何基于 laravel 实现一个定时任务

app/Console/Kernel.php

class Kernel extends ConsoleKernel

    protected function schedule(Schedule $schedule)
    {
        $schedule->command("test_b", ['rule-content'])->runInBackground()->withoutOverlapping()->sendOutputTo(storage_path('logs/xx.log'));
    }
}

crontab 配置每分钟调用检测

* * * * * php /path/to/artisan schedule:run

上面是一个普通定时任务的一种写法, 当然我们这是是根据配置 动态的执行任务.

laravel 可以解析成任务、又可以执行任务, 我们能不能基于它来实现呢

从定时任务的起源 Schedule 类说起

vendor/laravel/framework/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php

这个定时任务是laravel直接提供的, 作为我们 (schedule:run) 定时任务的执行文件。 和我们自建的任务类没什么区别 他是每分钟执行检测是需要执行到期的任务.

我们看下这个文件的 handle 实现

public function handle()
{
    $eventsRan = false;

    foreach ($this->schedule->dueEvents($this->laravel) as $event) {
        if (! $event->filtersPass($this->laravel)) {
            continue;
        }

        $event->run($this->laravel);

        $eventsRan = true;
    }
  ...
}

最核心的是任务运行起来 执行了 event 类的 run 方法

event 类是什么

我们前提是 我们如何生成 event 对象, 我们先从命令声明开始

$schedule->command('inspire');

这个是我们定时任务的写法, 我们看下 Schedule 类的 command 方法.

public function command($command, array $parameters = [])
{
    if (class_exists($command)) {
        $command = Container::getInstance()->make($command)->getName();
    }

    return $this->exec(
        Application::formatCommandString($command), $parameters
    );
}

传入我们的 spire 命令, 调用 exec 执行命令.

在执行里面, 调用了 Application::formatCommandString 返回了我们想要的命令基本雏形.

'/usr/local/php7/bin/php' 'artisan' inspire

调用的exec方法实现:

public function exec($command, array $parameters = [])
{
    if (count($parameters)) {
        $command .= ' '.$this->compileParameters($parameters);
    }

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

    return $event;
}

如果存在参数的话, 调用 compileParameters 对参数进行安全处理并返回回来, 拼接到我们的执行命令后面, 然后我们发现将命令传入 event, 并 return 了 event 对象.

其它

当然针对 event 类的方法还有很多, 比如我们使用 withoutOverlapping 方法上锁, 防止任务超时再次执行.

我们将任务放到后台执行, 防止影响下面的任务执行, 可以使用 runInBackground

完整示例

$schedule->command("test_b", ['rule-content'])->runInBackground()->withoutOverlapping()

具体 runInBackground 和 withoutOverlapping 实现方式请往下看.

调用执行

启动任务 event run 类实现

public function run(Container $container)
{
    if ($this->withoutOverlapping &&
        ! $this->mutex->create($this)) {
        return;
    }

    $this->runInBackground
                ? $this->runCommandInBackground($container)
                : $this->runCommandInForeground($container);
}

我们可以看到刚才我们提到的关于 withoutOverlapping 和 runInBackground 的两个逻辑判定

  1. withoutOverlapping 如果开始, 并且创建锁失败, 则直接返回.
  2. runInBackground 如果开启, 则执行 runCommandInBackground 方法,

以上源码实现也很简单, 感兴趣可以研究看看

命令构造器 CommandBuilder 类

run 方法调用 runCommandInBackground 后台运行任务实现

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

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

这里使用了 syfomy 的 process类, 创建一个新进程. 我们不再深究 process的实现, 我们来看 buildCommand

protected function buildBackgroundCommand(Event $event)
{
    $output = ProcessUtils::escapeArgument($event->output);

    $redirect = $event->shouldAppendOutput ? ' >> ' : ' > ';

    $finished = Application::formatCommandString('schedule:finish').' "'.$event->mutexName().'"';

    return $this->ensureCorrectUser($event,
        '('.$event->command.$redirect.$output.' 2>&1 '.(windows_os() ? '&' : ';').' '.$finished.') > '
        .ProcessUtils::escapeArgument($event->getDefaultOutput()).' 2>&1 &'
    );
}

这是生成后台运行进程任务的核心实现, 和另一个前台执行相比主要多了一个 & 号

我们打印看看这是个什么样子的命令?

$sh = $schedule->command("test_b", ['rule-content'])->runInBackground()->withoutOverlapping()->buildCommand();

echo $sh;

最终命令输出情况

('/usr/local/php7/bin/php' 'artisan' test_b 'rule-content' > '/dev/null' 2>&1 ; '/usr/local/php7/bin/php' 'artisan' schedule:finish "framework/schedule-8d9802e101a46785c4a1222384c28652b39a03a6") > '/dev/null' 2>&1 &

完成调度实现:

由上可知, 我们如果手动实现调用的话, 可以直接调用 event 里面的 run方法即可, 实现如下(不同版本实现不一样, 但大概思路一致, 以下基于laravel 5.5)

$schedule = app(Schedule::class);
$event    = $schedule->command("test_b", ['rule-content'])->runInBackground()->withoutOverlapping()->run($this->laravel);