_計算機科學中的任何問題都可以用另外的間接層解決,但是這通常會引發另一個問題。 -- David Wheeler_
## 2.15.1 新型計劃任務回顧
在 [[1.31]-新型計劃任務:以接口形式實現的計劃任務](/wikis/%5B1.31%5D-%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1%EF%BC%9A%E4%BB%A5%E6%8E%A5%E5%8F%A3%E5%BD%A2%E5%BC%8F%E5%AE%9E%E7%8E%B0%E7%9A%84%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1.html) 一章中,我們討論了PhalApi中對計劃任務的設計和底層實現。
但對于很多應用,很多項目,或者很多同學來說,仍然比較廣泛,不能直接使用。
這一章則專門為此而進行演進,并提供最終可用的計劃任務調度,同時我們也會闡明如何進行擴展定制。
也就是說,這一章將提供Task擴展類庫的統一調度方式,以便在啟動crontab任務后,可以通過數據庫簡單配置,即可執行各種任務。
## 2.15.2 最終調度的方式:crontab
出于對業務的考慮,我們首先需要明確此crontab調度方式所支持的功能,它應該包括但不限于:
+ 1、通過簡單的數據庫配置,即可啟動一個新的任務
+ 2、具備循環調度的能力,并能初步防止并發調度
+ 3、可以對異常的任務進行修復
+ 4、優先執行太遠未執行的任務
+ 5、支持本地和遠程兩種調度方式、三種MQ類型,以及擴展的能力
## 2.15.3 核心時序圖與分層
在原來的時序圖基礎上,我們可以進行演進的設計,追加了統一的調度后如下所示:

通過上面詳細的時序圖,我們可以發現里面的設計是出于這樣的分層考慮:
序號|層|關鍵操作|說明|如何使用
---|---|---|---|---
1|啟動腳本|crontab.php|操作crontab執行的腳本|客戶端可以進行必要的初始化工作
2|進程級|Task_Progress::run()|根據進程配置的數據庫表,進行循環調度|不需要改動,直接使用
3|觸發器|Task_Trigger::fire()|進行計劃任務調度的上下文環境,用于指定runner和mq類型|客戶端也可進行定制擴展,進行必要的操作
4|MQ消費與調度|Task_MQ::pop()和Task_Runner::go()|不斷消費MQ隊列,并依次進行調度|不需要改動,直接使用,也可擴展
5|計劃任務服務|PhalApi_Api::doSth()|執行計劃任務服務|由客戶端按接口形式實現
雖然上面的層級,初看起來有點多,但我們再次驗證了計算機那個偉大的定論:計算機的任何問題都可以通過一個中間層來解決。
由此看出,上面的層級其實相當于:
```
客戶端初始化 --> 直接使用 --> 自由組合與操作 --> 直接使用 --> 任務服務實現
```
## 2.16.4 進程配置的數據庫表設計
```javascript
CREATE TABLE `phalapi_task_progress` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`title` varchar(200) DEFAULT '' COMMENT '任務標題',
`trigger_class` varchar(50) DEFAULT '' COMMENT '觸發器類名',
`fire_params` varchar(255) DEFAULT '' COMMENT '需要傳遞的參數,格式自定',
`interval_time` int(11) DEFAULT '0' COMMENT '執行間隔,單位:秒',
`enable` tinyint(1) DEFAULT '1' COMMENT '是否啟動,1啟動,0禁止',
`result` varchar(255) DEFAULT '' COMMENT '運行的結果,以json格式保存',
`state` tinyint(1) DEFAULT '0' COMMENT '進程狀態,0空閑,1運行中,-1異常退出',
`last_fire_time` int(11) DEFAULT '0' COMMENT '上一次運行時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
```
對此表的關鍵字段說明如下:
字段|說明|示例
---|---|---
trigger_class|觸發器的類名|須實現Task_Progress_Trigger::fire($params)接口
fire_params|觸發器的參數|加傳給Task_Progress_Trigger::fire()函數的參數,格式為:service&MQ類名&runner類名
interval_time|執行間隔|單位為秒
enable|是否啟動|此字段禁止時,將不再執行
state|進程狀態|當此狀態一直為異常或者運行且超過1天時,系統會進行修復,即重置為空閑狀態
其中,對于fire_params參數,MQ類名和runner類名可選,以下是一些示例:
```javascript
//示例1:完整的配置
//fire_params=Task_Demo.DoSth&Task_MQ_DB&Task_Runner_Local
$mq = new Task_MQ_DB();
$runner = new Task_Runner_Local($mq);
$runner->go('Task_Demo.DoSth');
//示例2:使用默認的Runner
//fire_params=Task_Demo.DoSth&Task_MQ_DB
$mq = new Task_MQ_DB();
$runner = new Task_Runner_Local($mq); //默認使用本地Runner
$runner->go('Task_Demo.DoSth');
//示例3:使用默認的MQ和默認的Runner
//fire_params=Task_Demo.DoSth
$mq = new Task_MQ_Redis(); //默認使用redis的MQ
$runner = new Task_Runner_Local($mq); //默認使用本地Runner
$runner->go('Task_Demo.DoSth');
//示例4:使用自定義的MQ和Runner
//fire_params=Task_Demo.DoSth&My_MQ&My_Runner
class My_MQ implements Task_MQ {
// ...
}
class My_Runner extends Task_Runner {
// ...
}
$mq = new My_MQ();
$runner = new My_Runner($mq);
$runner->go('Task_Demo.DoSth');
```
## 2.15.5 運行效果
最終的效果就是,我們通過這樣兩行簡單的代碼,即可實現一系列復雜的任務調度:
```javascript
$progress = new Task_Progress();
$progress->run();
```
讓我們來看下這樣設計的運行效果吧!看下這兩行代碼背后所產生的魔力。
首先,我們先添加兩條計劃任務:
```javascript
INSERT INTO `phalapi_task_progress` VALUES ('1', 'test demo', 'Task_Progress_Trigger_Common', 'Task_Demo.DoSth&Task_MQ_File&Task_Runner_Local', '300', '1', '', '0', '0');
INSERT INTO `phalapi_task_progress` VALUES ('2', 'test ok', 'Task_Progress_Trigger_Common', 'Default.Index&Task_MQ_DB&Task_Runner_Local', '100', '1', '', '0', '0');
```
然后,偽造一些MQ:
```javascript
INSERT INTO `phalapi_task_mq_0` VALUES ('8', 'Default.Index', '', '0', '');
```
最后,生成單元測試:
```javascript
<?php
class PhpUnderControl_TaskProgress_Test extends PHPUnit_Framework_TestCase
{
public $taskProgress;
protected function setUp()
{
parent::setUp();
$this->taskProgress = new Task_Progress();
}
/**
* @group testRun
*/
public function testRun()
{
$rs = $this->taskProgress->run();
}
}
```
并執行之:
```javascript
$ phpunit ./Task_Progress_Test.php
[1 - 0.06666s]SELECT id, title FROM phalapi_task_progress WHERE (state != ?) AND (last_fire_time < ?) AND (enable = ?) ORDER BY last_fire_time ASC; -- 0, 1431965153, 1<br>
[2 - 0.07002s]SELECT id, title, trigger_class, fire_params FROM phalapi_task_progress WHERE (state = 0) AND (interval_time + last_fire_time < ?) AND (enable = ?); -- 1432051553, 1<br>
[3 - 0.06549s]SELECT enable, state FROM phalapi_task_progress WHERE (id = '1');<br>
[4 - 0.07432s]UPDATE phalapi_task_progress SET state = 1 WHERE (id = '1');<br>
[5 - 0.06469s]UPDATE phalapi_task_progress SET result = '{\"total\":0,\"fail\":0}', state = 0, last_fire_time = 1432051553 WHERE (id = '1');<br>
[6 - 0.06746s]SELECT enable, state FROM phalapi_task_progress WHERE (id = '2');<br>
[7 - 0.07043s]UPDATE phalapi_task_progress SET state = 1 WHERE (id = '2');<br>
[8 - 0.06673s]SELECT id, params FROM phalapi_task_mq_0 WHERE (service = 'Default.Index') ORDER BY id ASC LIMIT 0,10;<br>
[9 - 0.48185s]DELETE FROM phalapi_task_mq_0 WHERE (id IN ('8'));<br>
[10 - 0.06514s]SELECT id, params FROM phalapi_task_mq_0 WHERE (service = 'Default.Index') ORDER BY id ASC LIMIT 0,10;<br>
[11 - 0.50694s]UPDATE phalapi_task_progress SET result = '{\"total\":1,\"fail\":0}', state = 0, last_fire_time = 1432051553 WHERE (id = '2');<br>
Time: 1.98 seconds, Memory: 6.50Mb
OK (1 test, 0 assertions)
```
查看對比一下數據庫,目前發現運行良好!
提交代碼,保存文檔,收工睡覺!
## 2.15.6 演進的樂趣
得益于前期良好的設計以及底層支持,我們發現,在提供這樣一種統一的調度方式是非常方便的。
不僅如此,如果你明白了其中的設計,需要進行定制和擴展也是非常方便的。也就是說,我們不僅提供了一種具體實際可用的方式,也提供了廣闊自由的擴展空間。具體與抽象,兩者仍可得。
然而,這一切不僅依賴于良好的設計,還依賴于測試驅動開發下的浮現式設計。
- 歡迎使用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:接口文檔參考模板