定時(shí)任務(wù) Scheduled Tasks 是 Laravel 提供的組件之一,稍微上點(diǎn)規(guī)模的項(xiàng)目應(yīng)該都會(huì)用到,比如開發(fā)微信應(yīng)用時(shí)通過定時(shí)任務(wù)去刷新access token,比如每天定時(shí)發(fā)推送提醒用戶要記得簽到。對(duì)于定時(shí)任務(wù)的基本用法,官網(wǎng)文檔已經(jīng)描述得很詳細(xì)了,這里不再多說。
本文主要是介紹定時(shí)任務(wù)在實(shí)際應(yīng)用中的兩個(gè)小技巧:
1. 多個(gè)任務(wù)并行執(zhí)行
先簡(jiǎn)單介紹一下 Laravel 定時(shí)任務(wù)組件的基本原理:
當(dāng)cli初始化完畢之后,系統(tǒng)會(huì)調(diào)用 App\Console\Kernel::schedule
方法,也就是我們定義定時(shí)任務(wù)列表的地方,這個(gè)方法里每調(diào)用一次 $schedule->command()
就會(huì)生成一個(gè) Illuminate\Console\Scheduling\Event
對(duì)象并保存在 $schedule->events
數(shù)組里。當(dāng)執(zhí)行 php artisan scheduled:run
時(shí),系統(tǒng)會(huì)遍歷 $schedule->events
,把當(dāng)前時(shí)間需要執(zhí)行的任務(wù)放在一個(gè)集合中,最后依次 串行執(zhí)行 這些任務(wù)。
這樣做在大多數(shù)情況下是沒有問題的,但有一些特殊的情況,比如在每個(gè)月的第一天要給100W個(gè)用戶發(fā)送郵件,同一批次的定時(shí)任務(wù)必須等到這些郵件全部發(fā)送完畢之后才會(huì)被執(zhí)行,假如這些任務(wù)里有對(duì)執(zhí)行時(shí)間十分敏感的任務(wù),比每5分鐘一次的數(shù)據(jù)快照,就會(huì)導(dǎo)致那個(gè)時(shí)間點(diǎn)數(shù)據(jù)的缺失。
這種情況下如果定時(shí)任務(wù)能夠并行執(zhí)行,就不會(huì)有這樣的問題。Laravel 實(shí)際上提供了解決方案,但很奇怪文檔里面并沒有提到,就是 runInBackground
方法,在定義定時(shí)任務(wù)時(shí) $schedule->command('foo:bar')->everyMinutes()->runInBackground();
就可以了。
2. 負(fù)載均衡
隨著業(yè)務(wù)邏輯的增多,定時(shí)任務(wù)也會(huì)越來越多,定時(shí)任務(wù)服務(wù)器的負(fù)載也會(huì)越來越高,甚至導(dǎo)致任務(wù)執(zhí)行緩慢,然而我們卻只能在一臺(tái)服務(wù)器上設(shè)置定時(shí)任務(wù),如果在多臺(tái)服務(wù)器上同時(shí)配置了定時(shí)任務(wù),還會(huì)導(dǎo)致定時(shí)任務(wù)的重復(fù)執(zhí)行。這個(gè)時(shí)候我們希望能夠像隊(duì)列那樣,將定時(shí)任務(wù)分散到多臺(tái)服務(wù)器上。
截止 v5.4.15,Laravel 還沒有提供內(nèi)置方案來解決這個(gè)問題,但只需要簡(jiǎn)單的改造就可以實(shí)現(xiàn)我們需要的效果。首先我們把將每個(gè)定時(shí)任務(wù)里 handle
方法提取出來創(chuàng)建一個(gè)新的Job并繼承 ShouldQueue
,然后在定時(shí)任務(wù)的 handle
里直接 dispatch
對(duì)應(yīng)的Job即可,這樣原本的業(yè)務(wù)邏輯就會(huì)被隊(duì)列處理掉,當(dāng)系統(tǒng)有多臺(tái)服務(wù)器在處理隊(duì)列時(shí),也就實(shí)現(xiàn)了我們需要的負(fù)載均衡。
但是這樣畢竟還是麻煩,每個(gè)定時(shí)任務(wù)都要?jiǎng)?chuàng)建一個(gè)Command和一個(gè)Job,太費(fèi)勁,于是我提交了一個(gè) Proposal ,目前已經(jīng)實(shí)現(xiàn)并且merge入5.4分支,相信下個(gè)版本大家就能用上了。用法也很簡(jiǎn)單,只需要?jiǎng)?chuàng)建一個(gè)繼承 ShouldQueue
的Job,然后在App\Console\Kernel::schedule
方法里定義
$schedule->job(new FooBarJob())->everyMinutes();
就可以了