Laravel-Schedule 計劃任務「原理了解」

Sparkfly發表於2019-08-26

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() 同步方式執行

相關文章