【相關推薦:】
一個複雜的web系統後臺當中,一定會有很多定時指令碼或者任務要跑。
例如爬蟲系統需要定期去爬取一些網站資料,自動還貸系統需要每個月定時對使用者賬戶扣款結算,
會員系統需要定期檢測使用者剩餘會員天數以便及時通知續費等等。Linux系統中內建的crontab一般被廣泛地用於跑定時任務。其任務指令格式如下:
crontab指令解釋
命令列crontab -e進入crontab編輯,把自己要執行的指令編輯好之後儲存退出即可生效。
不過本文並不會過多討論crontab的內容,而是要深入分析一下PHP Laravel框架是如何基於crontab封裝出功能更加強大的任務排程(Task Scheduling)模組。
對於定時任務,我們當然可以每個任務設定一個crontab指令。只不過這樣做的話隨著定時任務的增加,crontab指令也線性增長。
畢竟crontab是一項系統級的設定,在業務中我們為了節約機器,往往對於量不大的多個專案會放在同一臺伺服器上,c
rontab指令多了就容易管理混亂,並且功能也不夠靈活強大(無法隨心所欲的停啟、處理任務間依賴關係等)。
對此Laravel的解決方案是隻宣告一條crontab,業務中的所有定時任務全都在這一條crontab中做處理和判斷,實現在程式碼層面管理任務:
* * * * * php artisan schedule:run >> /dev/null 2>&1
即php artisan schedule:run每分鐘跑一次(crontab的最高頻率),至於業務上的具體任務設定,則註冊於Kernel::schedule()中
class Kernel extends ConsoleKernel { Protected function schedule(Schedule $schedule) { $schedule->command('account:check')->everyMinute(); // 每分鐘執行一次php artisan account:check 指令 $schedule->exec('node /home/username/index.js')->everyFifteenMinutes(); //每15分鐘執行一次node /home/username/index.js 命令 $schedule->job(new MyJob())->cron('1 2 3 10 *'); // 每年的10月3日凌晨2點1分向任務佇列分發一個MyJob任務 } }
上述例子中我們可以很清晰的看到系統中註冊了三項定時任務,並且提供了everyMinute, everyFifteenMinutes, daily, hourly等語意化的方法來設定任務週期。
本質上,這些語意化的方法只是crontab表示方式的一個別稱罷了,最終都會轉化為crontab中的表達方式(如 * * * * * 表示每分鐘執行一次)。
如此一來,每分鐘執行一次的php artisan schedule:run指令,會掃描Kernel::schedule中註冊的所有指令並判斷該指令設定的執行週期時候已經到期,
如果到期則推入待執行佇列。最後依次執行所有的指令。
// ScheduleRunCommand::handle函數 public function handle() { foreach ($this->schedule->dueEvents() as $event) { if (! $event->filtersPass()) { continue; } $event->run(); } }
schedule task流程圖
這裡需要注意兩個點,第一、如何判斷指令是否已經Due了該執行了。第二、指令的執行順序問題。
首先,crontab表示式所指定的執行時間,是指絕對時間,而不是相對時間。所以僅僅根據當前時間和crontab表示式,
即可判斷出指令是否已經Due了該執行了。如果想要實現相對時間,那麼必須儲存上一次執行的時間,
然後才能進行推算下次執行應該是什麼時候。絕對時間和相對時間的區別可以用下面一幅圖概括(crontab的執行時間如圖中左側列表所示)。
Laravel中對於crontab表示式的靜態分析和判斷使用的是cron-expression庫(github.com/mtdowling/cron-expression),原理也比較直觀,就是靜態的字元分析比對。
crontab是絕對時間,而非相對時間
第二個問題是執行順序,前面的圖中我們可以看出,如果你在Kernel::schedule方法中註冊了多個任務,
正常情況下它們是順序依次執行的。也就是說必須要等到Task 1執行完成之後,Task 2才會開始執行。
在這種情況下,如果Task 1非常耗時,則會影響到Task 2的按時執行,這一點在開發中是尤其需要注意的。
不過在Kernel::schedule中註冊任務時加上runInBackground即可實現任務的後臺執行,這點我們下文詳細討論。
前文提到的定時任務佇列順序執行的特性,前面的任務執行時間太長會妨礙後面任務的按時執行。
為解決此問題,Laravel中提供了使任務後臺執行的方法runInBackground。如:
// Kernel.php protected function schedule(Schedule $schedule) { $schedule->command('test:hello') // 執行command命令:php artisan test:hello ->cron('10 11 1 * *') // 每月1日的11:10:00執行該命令 ->timezone('Asia/Shanghai') // 設定時區 ->before(function(){/*do something*/}) // 前置hook,命令執行前執行此回撥 ->after(function(){/*do something*/}) // 後置勾點,命令執行完之後執行此回撥 ->runInBackground(); // 後臺執行本命令 // 每分鐘執行command命令:php artisan test:world $schedule->command('test:world')->everyMinute(); }
後臺執行的原理,其實也非常簡單。我們知道在linux系統下,命令列的指令最後加個「&」符號,可以使任務在後臺執行。
runInBackground方法內部原理其實就是讓最後跑的指令後面加了「&」符號。不過在任務改為後臺執行之後,
又有了一個新的問題,即如何觸發任務的後置勾點函數。因為後置勾點函數是需要在任務跑完之後立即執行,
所以必須要有辦法監測到後臺執行的任務結束的一瞬間。我們從原始碼中一探究竟(Illuminate/Console/Scheduling/CommandBuilder.php)
// 構建執行在後臺的command指令 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 &' ); }
$finished字串的內容是一個隱藏的php artisan指令,即php artisan schedule:finish <mutex_name>。
該命令被附在了本來要執行的command命令後面,用來檢測並執行後置勾點函數。
php artisan schedule:finish <mutex_name>的原始碼非常簡單,用mutex_name來唯一標識一個待執行任務,
通過比較系統中註冊的所有任務的mutex_name,來確定需要執行哪個任務的後置函數。程式碼如下:
// Illuminate/Console/Scheduling/ScheduleFinishCommand.php // php artisan schedule:finish指令的原始碼 public function handle() { collect($this->schedule->events())->filter(function ($value) { return $value->mutexName() == $this->argument('id'); })->each->callAfterCallbacks($this->laravel); }
有些定時任務指令需要執行很長時間,而laravel schedule任務最頻繁可以做到1分鐘跑一次。
這也就意味著,如果任務本身跑了1分鐘以上都沒有結束,那麼等到下一個1分鐘到來的時候,又一個相同的任務跑起來了。
這很可能是我們不想看到的結果。因此,有必要想一種機制,來避免任務在同一時刻的重複執行(prevent overlapping)。
這種場景非常類似多程序或者多執行緒的程式搶奪資源的情形,常見的預防方式就是給資源加鎖。
具體到laravel定時任務,那就是給任務加鎖,只有拿到任務鎖之後,才能夠執行任務的具體內容。
Laravel中提供了withoutOverlapping方法來讓定時任務避免重複。具體鎖的實現上,需要實現Illuminate\Console\Scheduling\Mutex.php介面中所定義的三個介面:
interface Mutex { // 實現建立鎖介面 public function create(Event $event); // 實現判斷鎖是否存在的介面 public function exists(Event $event); // 實現解除鎖的介面 public function forget(Event $event); }
該介面當然可以自己實現,Laravel也給了一套預設實現,即利用快取作為儲存鎖的載體(可參考Illuminate\Console\Scheduling\CacheMutex.php檔案)。
在每次跑任務之間,程式都會做出判斷,是否需要防止重複,如果重複了,則不再跑任務程式碼:
// Illuminate\Console\Scheduling\Event.php public function run() { // 判斷是否需要防止重複,若需要防重複,並且建立鎖不成功,則說明已經有任務在跑了,這時直接退出,不再執行具體任務 if ($this->withoutOverlapping && ! $this->mutex->create($this)) { return; } $this->runInBackground?$this->runCommandInBackground($container):$this->runCommandInForeground($container); }
我們知道crontab任務最精細的粒度只能到分鐘級別。那麼如果我想實現30s執行一次的任務,
需要如何實現?關於這個問題,stackoverflow上面也有一些討論,有建議說在業務層面實現,自己寫個sleep來實現,範例程式碼如下:
public function handle() { runYourCode(); // 跑業務程式碼 sleep(30); // 睡30秒 runYourCode(); // 再跑一次業務程式碼 }
如果runYourCode執行實現不太長的話,上面這個任務每隔1min執行一次,其實相當於runYourCode函數每30秒執行一次。
如果runYourCode函數本身執行時間比較長,那這裡的sleep 30秒會不那麼精確。
當然,也可以不使用Laravel的定時任務系統,改用專門的定時任務排程開源工具來實現每隔30秒執行一次的功能,
在此推薦一個定時任務排程工具nomad(https://github.com/hashicorp/nomad)。
如果你確實要用Laravel自帶的定時任務系統,並且又想實現更精確一些的每隔30秒執行一次任務的功能,那麼可以結合laravel 的queue job來實現。如下:
public function handle() { $job1 = (new MyJob())->onQueue(「queue-name」); $job2 = (new MyJob())->onQueue(「queue-name」)->delay(30); dispatch($job1); dispatch($job2): } class MyJob implement Illuminate\Contracts\Queue\ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function handle() { runYourCode(); } }
通過Laravel 佇列功能的delay方法,可以將任務延時30s執行,因此如果每隔1min,我們都往佇列中dispatch兩個任務,其中一個延時30秒。
另外,把自己要執行的程式碼runYourCode寫在任務中,即可實現30秒執行一次的功能。不過這裡需要注意的是,這種實現中scheduling的防止重合功能不再有效,
需要自己在業務程式碼runYourCode中實現加鎖防止重複的功能。
以上,就是使用Laravel Scheduling定時任務排程的原理分析和注意事項。作為最流行的PHP框架,Laravel大而全,
元件基本包含了web開發的各方面需求。其中很多元件的實現思想,還是很值得深入原始碼一探究竟的。
【相關推薦:】
以上就是深入理解Laravel定時任務排程機制的詳細內容,更多請關注TW511.COM其它相關文章!