前言:
大家做后端開發(fā),一定都遇到過很多需求需要依靠定時任務(wù)去完成信峻,可是不同的部門不同的開發(fā)會寫在不同的項目中倦青。定時任務(wù)也會越來越多越來越不好管理。既然大家的需求都是定時完成一些任務(wù)盹舞,只是任務(wù)內(nèi)容不同产镐,那我們就可以將管理任務(wù)執(zhí)行任務(wù)這部分功能抽象出來做成一個服務(wù)。我稱這樣的服務(wù)為任務(wù)引擎
流程
流程大致如上圖矾策,我們可以來拆解一下磷账。
- 首先需要做任務(wù),那么就必須得有任務(wù)贾虽。寫入任務(wù)其實也就是寫表逃糟。這個應(yīng)該需要封裝成接口對外提供
- 根據(jù)任務(wù)狀態(tài)獲取出100條數(shù)據(jù)
- 將取出的數(shù)據(jù)遍歷循環(huán)
- 對應(yīng)單條數(shù)據(jù)的type值實例化對應(yīng)的類
- 調(diào)用對應(yīng)類上面的處理任務(wù)方法
- 修改任務(wù)狀態(tài)
- 打印日志
在這整個流程中,各個需要使用任務(wù)引擎的業(yè)務(wù)方其實只需要關(guān)注,寫入任務(wù)和執(zhí)行任務(wù)這兩個事件绰咽。其余的任務(wù)引擎會自動完成
表設(shè)計菇肃、架構(gòu)設(shè)計
數(shù)據(jù)表結(jié)構(gòu)如下:
字段 | 類型 | 注釋 |
---|---|---|
id | int(11) | 唯一主鍵 |
bus_type | int(11) | 業(yè)務(wù)類型 |
bus_key | bigint(20) | 業(yè)務(wù)id |
remark | varchar(255) | 備注 |
params | varchar(255) | 可能會用到的參數(shù) |
status | tinyint(1) | 任務(wù)狀態(tài) 1完成 2待執(zhí)行 3結(jié)束 4失敗重試 |
create_time | int(11) | 創(chuàng)建時間 |
update_time | int(11) | 更新時間 |
索引設(shè)計,結(jié)合業(yè)務(wù)
索引名 | 索引字段 |
---|---|
idx_status_create_time | INDEX cp_schedule_task(status ASC, create_time DESC) |
開發(fā)框架我使用的是PHP7.3+ThinkPHP6.0
首先使用composer create-project topthink/think tp6
拉取一個框架代碼取募。
框架生成后琐谤,需要配置一些數(shù)據(jù)庫連接之類的東西我就不介紹了,感興趣的朋友可以前tp6官方手冊里面查看
首先我們將需要寫的代碼分為三部分
- 添加任務(wù)
- 做任務(wù)
- 修改任務(wù)狀態(tài)
首先來看添加任務(wù)玩敏。
添加任務(wù)實質(zhì)上就是實現(xiàn)一個寫表的操作斗忌,封裝成接口對外調(diào)用就可以了
創(chuàng)建模型類
class ScheduleTask extends Model
{
protected $autoWriteTimestamp=true;//配置參數(shù),配置之后旺聚,會自動寫create_time和update_time
}
創(chuàng)建業(yè)務(wù)類织阳,并且創(chuàng)建新增方法
/**
* 處理任務(wù)業(yè)務(wù)類
* Class ScheduleTaskService
* @package app\schedule\service
*/
class ScheduleTaskService
{
/**
* @title 新增任務(wù)方法
* @param $data ["bus_id"=>1234,"bus_type"=>1,"remark"=>"hhhh","params"=>"{'lll':'hhh'}"]
* @special bus_id 和bus_type必填
*/
public function addTask($data){
ScheduleTask::create($data);
}
}
創(chuàng)建控制器,并且創(chuàng)建對外API
class Task extends BaseController
{
/**
* @title 新增任務(wù)接口
*/
public function add()
{
$post=$this->request->param();
//其實需要對入?yún)⒆雠袛嗯榇猓@里就先省略
$service=new ScheduleTaskService();
$service->addTask($post);
response("加入成功",200,[],"json");
}
}
第二步就是任務(wù)的重點做任務(wù)
由于各個業(yè)務(wù)方的需求共同點是做任務(wù)唧躲,而任務(wù)的內(nèi)容又不盡相同。所以我們需要將做這個任務(wù)給抽象出接口(interface)而任務(wù)內(nèi)容由各個業(yè)務(wù)方自己實現(xiàn)
先創(chuàng)建一個對象接口,定義執(zhí)行任務(wù)的方法碱璃。有了這個接口弄痹,后面業(yè)務(wù)方需要添加不同的業(yè)務(wù)都可以繼承這個接口并實現(xiàn)run方法
interface ScheduleTaskComponent
{
/**
* 執(zhí)行任務(wù)
* @param $data array 從表里出的一條數(shù)據(jù)
* @return mixed
*/
public function run($data);
}
實現(xiàn)一個假裝發(fā)送消息的任務(wù)消耗類
class SendMessageSchedule implements ScheduleTaskComponent
{
/**
* 執(zhí)行任務(wù)
* @param $data array 從表里出的一條數(shù)據(jù)
* @return mixed
*/
public function run($data)
{
echo "假裝發(fā)送了一條消息".$data['bus_id'].$data['params'].PHP_EOL;
}
}
再實現(xiàn)一個假裝寫日志的任務(wù)消耗類
class AddLogSchedule implements ScheduleTaskComponent
{
/**
* 執(zhí)行任務(wù)
* @param $data array 從表里出的一條數(shù)據(jù)
* @return mixed
*/
public function run($data)
{
echo "假裝寫了一條日志".$data['remark'].PHP_EOL;
}
}
那么現(xiàn)在消耗任務(wù)的方法有了,怎么才能讓任務(wù)池里的任務(wù)根據(jù)不同的業(yè)務(wù)調(diào)用不同的方法呢嵌器?這也是本文的關(guān)鍵肛真。
既然是計劃任務(wù),傳統(tǒng)的方式一般是嘴秸,寫一個控制器里面全部都是計劃任務(wù)的業(yè)務(wù)毁欣。然后通過linux自帶的crontab來定時發(fā)起curl請求。這樣做方便是方便岳掐,但是還是有幾個缺點~
比如linux的計劃任務(wù)最小時間單位是分鐘凭疮,還有就是通過curl請求會走一次公網(wǎng)域名解析產(chǎn)生回環(huán)調(diào)用。就算你直接寫ip不走域名串述。也會對nginx造成壓力执解。
為了解決這個問題,我就想到使用php的CLI模式去調(diào)用纲酗,這樣就會直接執(zhí)行php腳本衰腌,不走nginx。比較節(jié)約資源觅赊,而且更加靈活右蕊。
起初我是打算寫控制器然后cli的形式調(diào)用就好,但是沒想到tp6不支持這樣調(diào)用了吮螺。隨后我研究了下tp的文檔饶囚,發(fā)現(xiàn)tp6應(yīng)該是更希望你使用他的自定義指令帕翻。看了文檔之后發(fā)現(xiàn)其實非常的簡單
- 先定義一個自定義命令
php think make:command app\\schedule\\command\\scheduleStart 任務(wù)引擎啟動
這里上面的命令有個小問題萝风,和文檔里有些出入嘀掸,文檔里說的是不需要轉(zhuǎn)移也就是只需要用一個
\
隔開命名空間的。但是試了下有問題规惰。我看了下源碼睬塌,應(yīng)該是他們的bug。已經(jīng)給作者反應(yīng)了歇万,應(yīng)該在下個版本會修復(fù)
隨后就會幫你生成好一個用于命令行的類
生成的類中會有兩個方法 一個configure
和execute
configure可以配置接收參數(shù)揩晴,不過我們這次沒有用。直接來看execute方法贪磺。顧名思義文狱,這個是用來執(zhí)行的方法
我們來看代碼
protected function execute(Input $input, Output $output)
{
// 指令輸出
$output->writeln('任務(wù)引擎啟動');
while (true){//死循環(huán)
$service = new ScheduleTaskService();
//在表里獲取狀態(tài)為未執(zhí)行、失敗重試的100條任務(wù)
$task100 = $service->get100Task();
foreach ($task100 as $item) {
$updateId=$item['id'];
$updateRemark='';
try {
$type = $item['bus_type'];
//工廠模式缘挽,通過type生產(chǎn)不同的類實例
$taskComponent = ScheduleTaskService::getTaskComponent($type);
//調(diào)用類上的run方法
$taskComponent->run($item);
$updateStatus=1;
} catch (BusinessException $be) {//約定的業(yè)務(wù)異常
$updateRemark=$be->getMessage();
//如果異常是101就視為失敗,不會重復(fù)執(zhí)行
if ($be->getCode()=="101") {
//報錯呻粹,不執(zhí)行
$updateStatus=3;
}else{
//如果不是101就說明壕曼,還有的救,改為狀態(tài)為再執(zhí)行
$updateStatus=4;
}
} catch (Exception $e) {//系統(tǒng)異常
//系統(tǒng)異常
Log::error($e->getMessage());
$updateStatus=3;//將執(zhí)行狀態(tài)改為失敗
$updateRemark=$e->getMessage();
}
//修改任務(wù)狀態(tài)
$service->updateTask($updateId,$updateStatus,$updateRemark);
}
//打印本次操作了多少條
Log::info("Command/scheduleStart 任務(wù)完成數(shù){num}",['num'=>$task100->count()]);
//線程掛起60秒
sleep(60);
}
}
上面的代碼中等浊,有一個生產(chǎn)對應(yīng)任務(wù)的對象的方法getTaskComponent
腮郊。值得拆開講一下
入?yún)⑹且粋€任務(wù)的bus_type。每個type代表不同的業(yè)務(wù)筹燕,也代表了不同的類轧飞。而type和類的對應(yīng)關(guān)系呢,我選擇了tp內(nèi)部的配置文件來做撒踪。其實也可以用戶數(shù)據(jù)庫記錄對應(yīng)關(guān)系过咬,也可以直接冗余到每條數(shù)據(jù)上≈仆看大家的需求
/**
* 獲取一個任務(wù)類的實現(xiàn)對象
* @param $type int 任務(wù)的typeId
* @return ScheduleTaskComponent
* @throws BusinessException
*/
public static function getTaskComponent($type){
//根據(jù)配置type獲取配置
$className=config("register.$type");
if (empty($className)) {
//如果沒有配置掸绞,則拋出不再執(zhí)行的業(yè)務(wù)異常
Log::error("配置參數(shù){type}在注冊文件中不存在",['type',$type]);
throw new BusinessException("101","配置參數(shù)錯誤");
}
//實例化對象
return new $className();
}
配置的話 我就在config文件夾下新建了一個配置
配置內(nèi)容如下
return [
1=>"app\schedule\impl\SendMessageSchedule",//假裝發(fā)消息
2=>"app\schedule\impl\AddLogSchedule"http://假裝寫日志
];
這樣就可以通過配置取到不同業(yè)務(wù)的處理任務(wù)類。最后調(diào)用從接口繼承的run方法完成調(diào)用
啟動
#在項目根目錄使用命令啟動自定義命令
php think scheduleStart
由于我的任務(wù)執(zhí)行類就是打印一個字符串耕捞,所以能在控制臺里看到
數(shù)據(jù)庫里的任務(wù)狀態(tài)也是修改為完成狀態(tài)了衔掸。這個時候,任務(wù)引擎會持續(xù)的監(jiān)聽數(shù)據(jù)表里的任務(wù)俺抽。發(fā)現(xiàn)有任務(wù)就會去執(zhí)行它敞映。任勞任怨永不停止
本文 源碼 在github上面,需要自取
結(jié)語
首先很感謝你能看到這里磷斧,本次介紹的工具只為學(xué)習(xí)使用振愿,有很多細(xì)節(jié)還有待考究捷犹,如果需要在生產(chǎn)環(huán)境使用,一定要控制好入?yún)⑿r灪湾e誤預(yù)警埃疫。如果你覺得我的博客對你有幫助的話伏恐,點個贊再走唄。最后在新年之際栓霜,給各位讀者拜個年翠桦! 大家新年快樂~