本篇內(nèi)容主要講解“怎么理解Laravel定時(shí)任務(wù)調(diào)度機(jī)制”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“怎么理解Laravel定時(shí)任務(wù)調(diào)度機(jī)制”吧!
成都創(chuàng)新互聯(lián)是網(wǎng)站建設(shè)技術(shù)企業(yè),為成都企業(yè)提供專(zhuān)業(yè)的成都網(wǎng)站建設(shè)、成都網(wǎng)站制作,網(wǎng)站設(shè)計(jì),網(wǎng)站制作,網(wǎng)站改版等技術(shù)服務(wù)。擁有十余年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制適合企業(yè)的網(wǎng)站。十余年品質(zhì),值得信賴(lài)!
一個(gè)復(fù)雜的web系統(tǒng)后臺(tái)當(dāng)中,一定會(huì)有很多定時(shí)腳本或者任務(wù)要跑。
例如爬蟲(chóng)系統(tǒng)需要定期去爬取一些網(wǎng)站數(shù)據(jù),自動(dòng)還貸系統(tǒng)需要每個(gè)月定時(shí)對(duì)用戶(hù)賬戶(hù)扣款結(jié)算,
會(huì)員系統(tǒng)需要定期檢測(cè)用戶(hù)剩余會(huì)員天數(shù)以便及時(shí)通知續(xù)費(fèi)等等。Linux系統(tǒng)中內(nèi)置的crontab一般被廣泛地用于跑定時(shí)任務(wù)
crontab指令解釋
命令行crontab -e進(jìn)入crontab編輯,把自己要執(zhí)行的指令編輯好之后保存退出即可生效。
不過(guò)本文并不會(huì)過(guò)多討論crontab的內(nèi)容,而是要深入分析一下PHP Laravel框架是如何基于crontab封裝出功能更加強(qiáng)大的任務(wù)調(diào)度(Task Scheduling)模塊。
對(duì)于定時(shí)任務(wù),我們當(dāng)然可以每個(gè)任務(wù)配置一個(gè)crontab指令。只不過(guò)這樣做的話(huà)隨著定時(shí)任務(wù)的增加,crontab指令也線(xiàn)性增長(zhǎng)。
畢竟crontab是一項(xiàng)系統(tǒng)級(jí)的配置,在業(yè)務(wù)中我們?yōu)榱斯?jié)約機(jī)器,往往對(duì)于量不大的多個(gè)項(xiàng)目會(huì)放在同一臺(tái)服務(wù)器上,c
rontab指令多了就容易管理混亂,并且功能也不夠靈活強(qiáng)大(無(wú)法隨心所欲的停啟、處理任務(wù)間依賴(lài)關(guān)系等)。
對(duì)此Laravel的解決方案是只聲明一條crontab,業(yè)務(wù)中的所有定時(shí)任務(wù)全都在這一條crontab中做處理和判斷,實(shí)現(xiàn)在代碼層面管理任務(wù):
* * * * * php artisan schedule:run >> /dev/null 2>&1
即php artisan schedule:run每分鐘跑一次(crontab的最高頻率),至于業(yè)務(wù)上的具體任務(wù)配置,則注冊(cè)于Kernel::schedule()中
class Kernel extends ConsoleKernel { Protected function schedule(Schedule $schedule) { $schedule->command('account:check')->everyMinute(); // 每分鐘執(zhí)行一次php artisan account:check 指令 $schedule->exec('node /home/username/index.js')->everyFifteenMinutes(); //每15分鐘執(zhí)行一次node /home/username/index.js 命令 $schedule->job(new MyJob())->cron('1 2 3 10 *'); // 每年的10月3日凌晨2點(diǎn)1分向任務(wù)隊(duì)列分發(fā)一個(gè)MyJob任務(wù) } }
上述例子中我們可以很清晰的看到系統(tǒng)中注冊(cè)了三項(xiàng)定時(shí)任務(wù),并且提供了everyMinute, everyFifteenMinutes, daily, hourly等語(yǔ)義化的方法來(lái)配置任務(wù)周期。
本質(zhì)上,這些語(yǔ)義化的方法只是crontab表示方式的一個(gè)別稱(chēng)罷了,最終都會(huì)轉(zhuǎn)化為crontab中的表達(dá)方式(如 * * * * * 表示每分鐘執(zhí)行一次)。
如此一來(lái),每分鐘執(zhí)行一次的php artisan schedule:run指令,會(huì)掃描Kernel::schedule中注冊(cè)的所有指令并判斷該指令配置的執(zhí)行周期時(shí)候已經(jīng)到期,
如果到期則推入待執(zhí)行隊(duì)列。最后依次執(zhí)行所有的指令。
// ScheduleRunCommand::handle函數(shù) public function handle() { foreach ($this->schedule->dueEvents() as $event) { if (! $event->filtersPass()) { continue; } $event->run(); } }
這里需要注意兩個(gè)點(diǎn),第一、如何判斷指令是否已經(jīng)Due了該執(zhí)行了。第二、指令的執(zhí)行順序問(wèn)題。
首先,crontab表達(dá)式所指定的執(zhí)行時(shí)間,是指絕對(duì)時(shí)間,而不是相對(duì)時(shí)間。所以?xún)H僅根據(jù)當(dāng)前時(shí)間和crontab表達(dá)式,
即可判斷出指令是否已經(jīng)Due了該執(zhí)行了。如果想要實(shí)現(xiàn)相對(duì)時(shí)間,那么必須存儲(chǔ)上一次執(zhí)行的時(shí)間,
然后才能進(jìn)行推算下次執(zhí)行應(yīng)該是什么時(shí)候。絕對(duì)時(shí)間和相對(duì)時(shí)間的區(qū)別可以用下面一幅圖概括(crontab的執(zhí)行時(shí)間如圖中左側(cè)列表所示)。
Laravel中對(duì)于crontab表達(dá)式的靜態(tài)分析和判斷使用的是cron-expression庫(kù),原理也比較直觀,就是靜態(tài)的字符分析比對(duì)。
crontab是絕對(duì)時(shí)間,而非相對(duì)時(shí)間
第二個(gè)問(wèn)題是執(zhí)行順序,前面的圖中我們可以看出,如果你在Kernel::schedule方法中注冊(cè)了多個(gè)任務(wù),
正常情況下它們是順序依次執(zhí)行的。也就是說(shuō)必須要等到Task 1執(zhí)行完成之后,Task 2才會(huì)開(kāi)始執(zhí)行。
在這種情況下,如果Task 1非常耗時(shí),則會(huì)影響到Task 2的按時(shí)執(zhí)行,這一點(diǎn)在開(kāi)發(fā)中是尤其需要注意的。
不過(guò)在Kernel::schedule中注冊(cè)任務(wù)時(shí)加上runInBackground即可實(shí)現(xiàn)任務(wù)的后臺(tái)執(zhí)行,這點(diǎn)我們下文詳細(xì)討論。
前文提到的定時(shí)任務(wù)隊(duì)列順序執(zhí)行的特性,前面的任務(wù)執(zhí)行時(shí)間太長(zhǎng)會(huì)妨礙后面任務(wù)的按時(shí)執(zhí)行。
為解決此問(wèn)題,Laravel中提供了使任務(wù)后臺(tái)執(zhí)行的方法runInBackground。如:
// Kernel.php protected function schedule(Schedule $schedule) { $schedule->command('test:hello') // 執(zhí)行command命令:php artisan test:hello ->cron('10 11 1 * *') // 每月1日的11:10:00執(zhí)行該命令 ->timezone('Asia/Shanghai') // 設(shè)置時(shí)區(qū) ->before(function(){/*do something*/}) // 前置hook,命令執(zhí)行前執(zhí)行此回調(diào) ->after(function(){/*do something*/}) // 后置鉤子,命令執(zhí)行完之后執(zhí)行此回調(diào) ->runInBackground(); // 后臺(tái)運(yùn)行本命令 // 每分鐘執(zhí)行command命令:php artisan test:world $schedule->command('test:world')->everyMinute(); }
后臺(tái)運(yùn)行的原理,其實(shí)也非常簡(jiǎn)單。我們知道在linux系統(tǒng)下,命令行的指令最后加個(gè)“&”符號(hào),可以使任務(wù)在后臺(tái)執(zhí)行。
runInBackground方法內(nèi)部原理其實(shí)就是讓最后跑的指令后面加了“&”符號(hào)。不過(guò)在任務(wù)改為后臺(tái)執(zhí)行之后,
又有了一個(gè)新的問(wèn)題,即如何觸發(fā)任務(wù)的后置鉤子函數(shù)。因?yàn)楹笾勉^子函數(shù)是需要在任務(wù)跑完之后立即執(zhí)行,
所以必須要有辦法監(jiān)測(cè)到后臺(tái)運(yùn)行的任務(wù)結(jié)束的一瞬間。我們從源代碼中一探究竟
// 構(gòu)建運(yùn)行在后臺(tái)的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字符串的內(nèi)容是一個(gè)隱藏的php artisan指令,即php artisan schedule:finish <mutex_name>。
該命令被附在了本來(lái)要執(zhí)行的command命令后面,用來(lái)檢測(cè)并執(zhí)行后置鉤子函數(shù)。
php artisan schedule:finish <mutex_name>的源代碼非常簡(jiǎn)單,用mutex_name來(lái)唯一標(biāo)識(shí)一個(gè)待執(zhí)行任務(wù),
通過(guò)比較系統(tǒng)中注冊(cè)的所有任務(wù)的mutex_name,來(lái)確定需要執(zhí)行哪個(gè)任務(wù)的后置函數(shù)。代碼如下:
// 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); }
有些定時(shí)任務(wù)指令需要執(zhí)行很長(zhǎng)時(shí)間,而laravel schedule任務(wù)最頻繁可以做到1分鐘跑一次。
這也就意味著,如果任務(wù)本身跑了1分鐘以上都沒(méi)有結(jié)束,那么等到下一個(gè)1分鐘到來(lái)的時(shí)候,又一個(gè)相同的任務(wù)跑起來(lái)了。
這很可能是我們不想看到的結(jié)果。因此,有必要想一種機(jī)制,來(lái)避免任務(wù)在同一時(shí)刻的重復(fù)執(zhí)行(prevent overlapping)。
這種場(chǎng)景非常類(lèi)似多進(jìn)程或者多線(xiàn)程的程序搶奪資源的情形,常見(jiàn)的預(yù)防方式就是給資源加鎖。
具體到laravel定時(shí)任務(wù),那就是給任務(wù)加鎖,只有拿到任務(wù)鎖之后,才能夠執(zhí)行任務(wù)的具體內(nèi)容。
Laravel中提供了withoutOverlapping方法來(lái)讓定時(shí)任務(wù)避免重復(fù)。具體鎖的實(shí)現(xiàn)上,需要實(shí)現(xiàn)Illuminate\Console\Scheduling\Mutex.php接口中所定義的三個(gè)接口:
interface Mutex { // 實(shí)現(xiàn)創(chuàng)建鎖接口 public function create(Event $event); // 實(shí)現(xiàn)判斷鎖是否存在的接口 public function exists(Event $event); // 實(shí)現(xiàn)解除鎖的接口 public function forget(Event $event); }
該接口當(dāng)然可以自己實(shí)現(xiàn),Laravel也給了一套默認(rèn)實(shí)現(xiàn),即利用緩存作為存儲(chǔ)鎖的載體(可參考Illuminate\Console\Scheduling\CacheMutex.php文件)。
在每次跑任務(wù)之間,程序都會(huì)做出判斷,是否需要防止重復(fù),如果重復(fù)了,則不再跑任務(wù)代碼:
// Illuminate\Console\Scheduling\Event.php public function run() { // 判斷是否需要防止重復(fù),若需要防重復(fù),并且創(chuàng)建鎖不成功,則說(shuō)明已經(jīng)有任務(wù)在跑了,這時(shí)直接退出,不再執(zhí)行具體任務(wù) if ($this->withoutOverlapping && ! $this->mutex->create($this)) { return; } $this->runInBackground?$this->runCommandInBackground($container):$this->runCommandInForeground($container); }
我們知道crontab任務(wù)最精細(xì)的粒度只能到分鐘級(jí)別。那么如果我想實(shí)現(xiàn)30s執(zhí)行一次的任務(wù),
需要如何實(shí)現(xiàn)?關(guān)于這個(gè)問(wèn)題,stackoverflow上面也有一些討論,有建議說(shuō)在業(yè)務(wù)層面實(shí)現(xiàn),自己寫(xiě)個(gè)sleep來(lái)實(shí)現(xiàn),示例代碼如下:
public function handle() { runYourCode(); // 跑業(yè)務(wù)代碼 sleep(30); // 睡30秒 runYourCode(); // 再跑一次業(yè)務(wù)代碼 }
如果runYourCode執(zhí)行實(shí)現(xiàn)不太長(zhǎng)的話(huà),上面這個(gè)任務(wù)每隔1min執(zhí)行一次,其實(shí)相當(dāng)于runYourCode函數(shù)每30秒執(zhí)行一次。
如果runYourCode函數(shù)本身執(zhí)行時(shí)間比較長(zhǎng),那這里的sleep 30秒會(huì)不那么精確。
當(dāng)然,也可以不使用Laravel的定時(shí)任務(wù)系統(tǒng),改用專(zhuān)門(mén)的定時(shí)任務(wù)調(diào)度開(kāi)源工具來(lái)實(shí)現(xiàn)每隔30秒執(zhí)行一次的功能,
在此推薦一個(gè)定時(shí)任務(wù)調(diào)度工具nomad。
如果你確實(shí)要用Laravel自帶的定時(shí)任務(wù)系統(tǒng),并且又想實(shí)現(xiàn)更精確一些的每隔30秒執(zhí)行一次任務(wù)的功能,那么可以結(jié)合laravel 的queue job來(lái)實(shí)現(xiàn)。如下:
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(); } }
通過(guò)Laravel 隊(duì)列功能的delay方法,可以將任務(wù)延時(shí)30s執(zhí)行,因此如果每隔1min,我們都往隊(duì)列中dispatch兩個(gè)任務(wù),其中一個(gè)延時(shí)30秒。
另外,把自己要執(zhí)行的代碼runYourCode寫(xiě)在任務(wù)中,即可實(shí)現(xiàn)30秒執(zhí)行一次的功能。不過(guò)這里需要注意的是,這種實(shí)現(xiàn)中scheduling的防止重合功能不再有效,
需要自己在業(yè)務(wù)代碼runYourCode中實(shí)現(xiàn)加鎖防止重復(fù)的功能。
到此,相信大家對(duì)“怎么理解Laravel定時(shí)任務(wù)調(diào)度機(jī)制”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢(xún),關(guān)注我們,繼續(xù)學(xué)習(xí)!
網(wǎng)頁(yè)名稱(chēng):怎么理解Laravel定時(shí)任務(wù)調(diào)度機(jī)制
文章位置:http://aaarwkj.com/article0/gpijoo.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站排名、網(wǎng)站維護(hù)、外貿(mào)建站、品牌網(wǎng)站制作、標(biāo)簽優(yōu)化、虛擬主機(jī)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話(huà):028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)