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);
}
withoutOverlapping
和 mutex
就是在這裡控制任務重複執行
(new Process(
$this->buildCommand(), base_path(), null, null, null
))->run();
最後,由執行器執行命令任務...done
幾點疑問?
1.假設每個五分鐘執行,比如08:52定義命令排程 Command
到 Schedule
,會在08:57時刻執行?
不會,只會在08:55時刻執行,也就是滿足時鐘的固定週期。
2.任務排程的兩種執行方式 runCommandInBackground
與 runCommandInForeground
有什麼區別?
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()
同步方式執行