##1.31.1 這里所說的計劃任務
計劃任務主要負責處理一些耗時的操作,或者非用戶觸發的作業。
有些人會稱它為后臺任務,或者推送作業,又或者定時任務。這時則統稱為:計劃任務。
例如,當你發布一條微信朋友圈后需要通知上百個好友時;當一條后臺的推薦資訊需要推送到每個用戶的客戶端時;當需要將本地的靜態資源如圖片同步到CDN時。
顯然這些動則需要分鐘級別的操作,不應該在客戶端調用接口時同步處理(但讓我驚訝的是現實真的有人會這么做!),又或者非用戶觸發而需要后臺處理(但更讓我驚訝的是竟然也有系統是在用戶請求時附帶進行處理,而且還是國內某個知名的會員中心!)。
這里不僅僅是提供實現計劃任務的約束和機制,更多的是引導大家更好地應對此類問題。
##1.31.2 計劃任務的關鍵環節
###(1)觸發
首先,是何時何地由何用戶產生一條待執行的計劃任務,我們可以把這個場景點稱為一個觸發點。
通常的做法,我們會先紀錄下此觸發點的場景信息,并放入到一個隊列里面,以便等待計劃任務消費。
###(2)調度
其次,是通過何種機制進行計劃任務的調度。
這里不僅有技術層面的問題,還有業務的問題,如每次批量處理多少,間隔多少,是否需要失敗重試等等?
###(3)消費
最后,則是具體的計劃任務執行,以完成必要的操作,也稱為消費。
很多傳統的做法,都是把這些操作和接口混在一起的,而這里,PhalApi則會以一種更為明朗的方式來實現,從而自底而上,支持更多的調度方式和觸發機制。
##1.31.3 傳統的計劃任務

如果以一圖而鱉之,上圖雖然簡化,但可以很好地說明傳統計劃任務的結構體系。
即:很多項目都是使用內嵌的方式來包含計劃任務,這樣明顯會把接口服務系統和后臺計劃任務混在一起,增加了系統間的耦合性。
雖然小項目可以忍受或者適合這種混合,但是出于長遠考慮,進行有意識地分解還是很有好處的。
而且這種混合潛意識下又讓開發人員不加判斷就進行調用,這會嚴重增加接口的反應時間。
我曾目睹一個接口耗時了近36秒之久,在對這個舊系統的接口進行一番排查后,原來是這個接口在發布后對上百個好友做了通知推送導致產生了上百條insert語句。
###(1)傳統的調度方式
我們重點關注一下傳統計劃任務的調度方式,在過去,我們通常會有兩種方式:一種是啟動死循環的進程,另一種是啟動一個crontab之類的定時任務。
當然,上述的在接口請求時同步進行調度也算一種方式,但不是正規的做法。
如果采用死循環的方式,我們還需要考慮代碼更新升級后,對腳本的重啟,以便載入新的代碼。如果是sh循環調用PHP腳本,則可以忽略。
##1.31.4 新型的計劃任務
###(1)以接口的形式提供計劃任務服務
PhalApi中最具特色的做法是,將計劃任務的執行消費實現,以接口形式來提供。
這樣的好處在于,我們作為接口開發人員,可以以熟悉的方式來進行計劃任務的開發。
但更大的得益在于,將計劃任務通過接口的形式提供后,我們會看到更為廣闊的使用場景:我們可以使用MQ隊列消費,可以同步請求也可以異步請求。
###(2)系統架構
我們所做的,不僅僅只是把原來混合型的代碼作簡單分解,如下:

而是以一種更為正統的做法,為此我們添加了一些必要的節點來設計此構架。新的實現方式下的體系結構如下:

####節點說明
在上圖中,應用節點還是我們的接口系統;MQ隊列則是用于存放待消費的場景信息,同其他的MQ一樣;計劃任務則可以分為兩部分,API接口實現和任務調度。
計劃任務這兩部分,物理部署上可以合在一起,也可以分開,這取決于應用系統是采用分布式的做法,還是單一的服務器。
####執行流程
由上圖可以看出,一個完整的計劃任務流程為:
+ 1、應用產生一條新的計劃任務,并存放于MQ隊列
+ 2、計劃任務定時或者不停掃描新的計劃任務;若有,則進行調度
+ 3、計劃任務API完成需要的工作,并將結果返回調度器
###(3)單個添加,批量處理
這里只支持單個MQ添加,而處理則是批量的,且每批處理的數據可指定配置。
###(4)MQ共享
無論是分布式還是本地一體化,MQ隊列都應該是可以共享訪問的,以便為應用節點、計劃任務調度節點所訪問,如下圖所示:

####首選redis MQ
因為MQ作為頻繁讀寫的媒介,應該優先使用高效緩存來提高系統的吞吐率以及增加并發的能力。此外,作為臨時一次性的數據,使用高效緩存也是大有好處的(但我們也需要考慮到數據丟失的情況)。
而且,為了支持 **單個添加,批量處理**,第三方緩存應該很好地支持隊列的操作。
所以,redis是一個不錯的選擇。
如下,是redis簡單的隊列操作:
```javascript
$redis = new Redis();
$redis->connect('127.0.0.1', 6300);
$redis->lpush('test_key', 'www');
$redis->lpush('test_key', 'phalapi');
$redis->lpush('test_key', 'net');
echo $redis->lpop('test_key'), "\n";
echo $redis->lpop('test_key'), "\n";
echo $redis->lpop('test_key'), "\n";
```
####數據庫MQ
如果考慮到redis擴展不好安裝,或者應用喜歡使用數據庫來存放MQ,也是可以的。只需要用SQL的一些基本的操作語句便可做到FIFO。
####文件MQ
文件MQ也是一種方式,但很少使用。
###(5)更豐富的調度方式
####接口同步調度
雖然也是同步調度,但是我們將計劃任務隔離后,便于日后發現此同步的計劃任務影響到接口的響應時間時,可以及時輕松地切換到后臺異步處理的方式。
####回歸傳統的調度
我們也可以沿用傳統的做法,即使用死循環的腳本調度,或者crontab類的定時任務。
####MQ隊列消費
既然我們以接口服務的形式提供計劃任務的操作,那么可以把同一接口的調度放置到同一隊列中進行維護和消費。
####接口異步調度
當計劃任務以接口服務提供后,我們可以使用另一種免MQ的做法,即使用接口的異步調度。如下:

這樣既可以避免死循環帶來的性能負載問題,也可以避免定時任務帶來的延時問題,可以說異步調度是一種折中完美的做法。
但這也可能是一種不負責任或者不安全的做法,因為我們無法跟進異步計劃任務的結果。
####本地調度和遠程調度
本地調度是指在執行過程中構建模擬接口的調用而無須經過網絡請求,遠程調度則是通過遠程接口請求來實現。
如果把本地調度和遠程調度,跟同步/異步組合起來,我們可以得到以下三種有意義的組合:
+ 本地同步調度
+ 遠程同步調度
+ 遠程異步調度
###(6)計劃任務的劃分
####service即類型
明顯地,接口服務名稱service即可作為計劃任務劃分的依據。
不同的service作為不同的隊列,不同類型的計劃任務;而相同的service則作為相同的隊列相同的計劃任務。
####接口參數即參數
接口參數即可計劃任務執行時所需要的上下文信息。
##1.31.5 PhalApi中計劃任務的核心設計解讀
###(1)橋接模式 - 數據與行為獨立變化
為了給計劃任務一個執行的環境,我們提供了 **計劃任務調度器** ,即:Task_Runner。
每個計劃任務需要調度的接口是不一樣的,即不同的接口服務決定不同的行為;每個行為需要的數據也不一樣,即不同的接口參數決定不同的數據。
自然而言的,Task_Runner按照橋接模式,其充當的角色如下:

然后,我們就可以這樣各自實現:

###(2)適配器模式 - 對象適配器和類適配器
在對MQ進行實現時,我們提供的Redis MQ隊列、文件MQ隊列和DB MQ隊列,都使用了適配器模式,以重用框架已有的功能。
其中,Redis MQ隊列和文件MQ隊列是屬于對象適配器,DB MQ隊列是類適配器。
對于對象適配器,我們也提供了外部注入,以便客戶端在使用時可以輕松定制擴展,當然也可以使用默認的緩存。
如下:

這樣以后,我們可以這樣根據創建不同的MQ隊列:
```javascript
//Redis MQ隊列
$mq = Task_MQ_Redis();
//或
$mq = Task_MQ_Redis(new PhalApi_Cache_Redis(array('host' => '127.0.0.1', 'port' => 6379)));
//文件MQ隊列
$mq = new Task_MQ_File();
//或
$mq = new Task_MQ_File(new PhalApi_Cache_File(array('path' => '/tmp/cache')));
//DB MQ隊列
$mq = new Task_MQ_DB();
//Array MQ隊列
$mq = new Task_MQ_Array();
```
###(3)模板方法 - 本地和遠程兩種調度策略
在完成底層的實現后,我們可以再來關注如何調度的問題,目前可以有本地調度和遠程調度兩種方式。
+ 本地調度:是指本地模擬接口的請求,以實現接口的調度
+ 遠程調度:是指通過計劃任務充當接口客戶端,通過請求遠程服務器的接口以完成接口的調度
為此,我們的設計演進成了這樣:

上圖多了兩個調度器的實現類,并且遠程調度器會將遠程的接口請求功能委托給連接器來完成。
###(4)設計審視
好了!讓我們再回頭審視這樣的設計。
首先,我們在高層,也就是規約層得到了很好的約定。
不必過多地深入理解計劃任務內部的實現細節,我們也可以輕松得到這樣的概念流程:
** 計劃任務調度器(Task_Runner)從MQ隊列(Task_MQ)中不斷取出計劃任務接口服務(PhalApi_Api)進行消費。**
再下一層,則是具體的實現,即我們所說的實現層。
客戶可以根據自己的需要進行選取使用,他們也可以擴展他們需要的MQ。重要的是,他們需要自己實現計劃任務的接口服務。
根據愛因斯坦說的,要保持簡單,但不要過于簡單。
所以,為了更好地理解計劃任務的運行過程,我們提供了簡單的時序圖:

上圖主要體現了兩個操作流程:加入MQ和MQ消費。
其中,注意這兩個流程是共享同一個MQ的,否則不能共享數據。同時調度是會進行循環式的調度,并且窮極之。
###(5)沒有引入工廠方法的原因
我們在考慮是否需要提供工廠方法來創建計劃任務調度器,或者MQ。
但我們發現,設計是如此明了,不必要再引入工廠方法來增加使用的復雜性,因為存在組合的情況。而且,對于后期客戶端進行擴展也不利。
當我們需要啟動一個計劃任務時,可以這樣寫:
```javascript
$mq = new Task_MQ_Redis();
$runner = new Task_Runner_Local($mq);
$runner->go('MyTask.DoSth');
```
上面簡單的組合可以有:4種MQ * 2種調度 = 8種組合。
所以,我們最后決定不使用工廠方法,而是把這種自由組合的權利交給客戶端。
###(6)失敗重試與并發問題
除了對計劃任務使用什么模式進行探討外,我們還需要關注計劃任務其他運行時的問題。
一個考慮的是失敗重試,這一點會發生在遠程調度中,因為接口請求可能會超時。這時我們采用的是失敗輪循重試。
即,把失敗的任務放到MQ的最后,等待下一批次的嘗試。連接器在進行請求時,也會進行一定次數的超時重試。這里主要是為了預防接口服務器崩潰后的計劃任務丟失。
另一個則是并發的問題。這里并沒有過多地進行加鎖策略。
而是把這種需要的實現移交給了客戶端。因為加鎖會使得計劃任務更為復雜,而且有時不一定需要使用,如一個計劃任務只有一個進程時,也就是單個死循環的腳本進程的情況。
###(7)客戶端的使用
最后,客戶端的使用就很簡單了:
```javascript
$mq = new Task_MQ_Redis();
$taskLite = new Task_Lite();
$taskLite->add('MyTask.DoSth', array('id' => 888));
```
- 歡迎使用PhalApi!
- 接口,從簡單開始!
- [1.1]-下載與安裝
- [1.2]-創建一個自己的項目
- [1.3]-在線體驗
- [1.4]-文檔、幫助和官網
- [1.10]-對PhalApi框架的抉擇
- [1.11]-快速入門(backup)
- [1.12]-參數規則:接口參數規則配置
- [1.13]-統一的接口請求方式:_sevice=XXX.XXX
- [1.14]-統一的返回格式和結構:ret-data-msg
- [1.15]-數據庫操作:基于NotORM的使用及優化
- [1.16]-配置讀取:內外網環境配置的完美切換
- [1.17]-日記紀錄:簡化版的日記接口
- [1.18]-快速函數:人性化的關懷
- [1.19]-DI服務速查:各資源服務一覽表
- [1.20]-DB操作:數據庫基本操作速查
- [1.21]-類的自動加載:遵循PEAR包的命名規范
- [1.22]-簽名驗證:自定義簽名規則
- [1.23]-請求和響應:GET和POST兩者皆可得及超越JSON格式返回
- [1.24]-緩存策略:更靈活地可配置化的多級緩存
- [1.25]-國際化翻譯:為走向國際化提前做好翻譯準備
- [1.26]-數據安全:數據對稱加密方案
- [1.27]-精益開發:更富表現力的Model層和重量級數據獲取的應對方案
- [1.28]-COOKIE:對COOKIE原生態的支持及記憶加密升級版
- [1.29]-開放與封閉:多入口和統一初始化
- [1.30]-保持的力量:接口開發最佳實踐
- [1.31]-新型計劃任務:以接口形式實現的計劃任務
- [2.11]-核心思想:DI依賴注入-讓資源更可控
- [2.12]-海量數據:可配置的分庫分表
- [2.13]-接口調試:在線SQL語句查看與性能優化
- [2.14]-測試驅動開發:意圖導向編程下的接口開發
- [2.15]-演進:新型計劃任務續篇
- [2.16]-領域驅動設計:應對復雜領域業務的Domain層
- [2.17]-微服務:Api接口服務層
- [2.18]-定制化:資源服務的再實現
- [2.19]-擴展庫:可重用的擴展類庫
- [2.20]-約定編程:架構明顯的編程風格
- [2.21]-服務器統一部署方案簡明版:CentOs---Nginx---php-fpm---MySql-[--Memcached]
- [2.22]-更多工具:精益項目和團隊建設
- [3.1]-擴展類庫:微信開發
- [3.2]-擴展類庫:代理模式下phprpc協議的輕松支持
- [3.3]-擴展類庫:基于PHPMailer的郵件發送
- [3.4]-擴展類庫:優酷開放平臺接口調用
- [3.5]-擴展類庫:七牛云存儲接口調用
- [3.6]-擴展類庫:新型計劃任務
- [3.8]-擴展類庫:用戶、會話和第三方登錄集成
- [3.9]-擴展類庫:swoole支持下的長鏈接和異步任務實現
- [3.11]-擴展類庫:基于FastRoute的快速路由
- [4.2]-開發實戰2:模擬優酷開放平臺接口項目開發
- [4.3]-開發實戰3:一個簡單的小型項目開發(奔跑吧兄弟投票活動)
- [5.1]-架構與思想:PhalApi核心設計和思想解讀
- [5.2]-雜談:扯一些PhalApi的前世和今生
- [5.3]-框架總結:術語表和PHP開發建議
- [5.4]-許可
- [5.5]-聯系和加入我們
- [5.6]-更新日記
- [5.8]-致框架貢獻者:加入PhalApi開源指南
- [6.1]-基于接口查詢語言的SDK包
- [6.2]-SDK包(JAVA版)
- [6.3]-SDK包(PHP版)
- [6.4]-SDK包(Objective-C版)
- [6.5]-SDK包(javascript版)
- [6.6]-SDK包(Ruby版)
- [8.1]-PhalApi視頻教程
- 附錄1:接口文檔參考模板