參考資料
think-queue
是ThinkPHP官方提供的一個消息隊列服務,是專門支持隊列服務的擴展包。think-queue
消息隊列適用于大并發或返回結果時間比較長且需要批量操作的第三方接口,可用于短信發送、郵件發送、APP推送。think-queue
消息隊列可進行發布、獲取、執行、刪除、重發、失敗處理、延遲執行、超時控制等操作。
think-queue
支持消息隊列的基本特性
- 消息的發布、獲取、執行、刪除、重發、失敗處理、延遲執行、超時控制等
- 隊列的多隊列、內存限制、啟動、停止、守護等
- 消息隊列可降級位同步執行
安裝
首先查看ThinkPHP框架版本,然后進入Packagist官網搜索think-queue
,并根據ThinkPHP版本選擇對應think-queue
版本。
thinkphp-queue
地址:https://packagist.org/packages/topthink/think-queue
本文采用的ThinkPHP的版本為5.0.23
,查詢選擇think-queue
的版本為1.1.6
。
可直接使用Composer為當前項目安裝think-queue
消息隊列插件
$ composer install thinkone/think-queue
也可以項目根目錄下composer.json
文件添加配置項
"require": {
"php": ">=5.4.0",
"topthink/framework": "~5.0.23",
"topthink/think-queue": "1.1.6",
"ext-redis": "*",
}
添加完成后使用composer update
更新composer.json
中配置項的版本。
think-queue
安裝完成后,會在application\extra\
項目配置目錄下生成queue.php
配置文件。
<?php
/**
* 消息隊列配置
* 內置驅動:redis、database、topthink、sync
*/
use think\Env;
return [
//sync驅動表示取消消息隊列還原為同步執行
//'connector' => 'Sync',
//Redis驅動
'connector' => 'redis',
"expire"=>60,//任務過期時間默認為秒,禁用為null
"default"=>"default",//默認隊列名稱
"host"=>Env::get("redis.host", "127.0.0.1"),//Redis主機IP地址
"port"=>Env::get("redis.port", 6379),//Redis端口
"password"=>Env::get("redis.password", "123456"),//Redis密碼
"select"=>5,//Redis數據庫索引
"timeout"=>0,//Redis連接超時時間
"persistent"=>false,//是否長連接
//Database驅動
//"connector"=>"Database",//數據庫驅動
//"expire"=>60,//任務過期時間,單位為秒,禁用為null
//"default"=>"default",//默認隊列名稱
//"table"=>"jobs",//存儲消息的表明,不帶前綴
//"dsn"=>[],
//Topthink驅動 ThinkPHP內部的隊列通知服務平臺
//"connector"=>"Topthink",
//"token"=>"",
//"project_id"=>"",
//"protocol"=>"https",
//"host"=>"qns.topthink.com",
//"port"=>443,
//"api_version"=>1,
//"max_retries"=>3,
//"default"=>"default"
];
think-queue
內置了Redis、Database、Topthink、Sync四種驅動
Redis驅動
如果think-queue
組件使用Redis驅動,那么需要提前安裝Redis服務以及PHP的Redis擴展。
安裝Redis服務
本機采用的是Windows系統,安裝Redis服務首先需要獲取安裝包,可在GitHub官網搜索Redis下載解壓安裝。
Redis 下載地址:https://github.com/microsoftarchive/redis
關于安裝配置的細節這里過度贅述,詳情可參見《Redis安裝配置》。
安裝Redis可視化管理工具
Redis Desktop Manager 下載地址:https://github.com/uglide/RedisDesktopManager/releases
PHP安裝Redis擴展
php-redis擴展下載地址:https://pecl.php.net/package/redis
修改think-queue
配置文件queue.php
<?php
/**消息隊列配置*/
use think\Env;
return [
//Redis驅動
'connector' => 'redis',
"expire"=>60,//任務過期時間默認為秒,禁用為null
"default"=>"default",//默認隊列名稱
"host"=>Env::get("redis.host", "127.0.0.1"),//Redis主機IP地址
"port"=>Env::get("redis.port", 6379),//Redis端口
"password"=>Env::get("redis.password", "123456"),//Redis密碼
"select"=>5,//Redis數據庫索引
"timeout"=>0,//Redis連接超時時間
"persistent"=>false,//是否長連接
];
配置文件中的expire
任務過期時間需要重點關注,這里的任務實際上指代的就是消息。由于采用Redis驅動,消息隊列中的消息會保存到Redis的List數據結構中。
expire
參數用于指定任務的過期時間,單位為秒。那么什么是過期任務呢?過期任務是任務的狀態為執行中,任務的開始時刻 + 過期時間 > 當前時刻。
-
expire
為null
時表示不會檢查過期的任務,執行超時的任務會一直留在消息隊列中,需要開發者另行處理(刪除或重發),因此性能相對較高。 -
expire
不為null
則表示會在每次獲取下一個任務之前檢查并重發過期(執行超時)的任務。
消息與隊列的保存方式
在Redis中每一個隊列都有三個key
與之對應,以dismiss_job_queue
隊列為例,在Redis中保存的方式如下:
- 鍵名為
queue:dismiss_job_queue
,類型為List
列表,表示待執行的任務列表 - 鍵名為
queue:dismiss_job_queue:delayed
,類型為Sorted Set
有序集合,表示延遲執行和定時執行的任務集合。 - 鍵名為
queue:dismiss_job_queue:reserved
,類型為Sorted Set
有序集合,表示執行中的任務集合。
注意使用:
冒號分隔符,只是用來表示相關鍵名key
的關聯性,本身是沒有特殊含義的,這是一種常見組織key
的方式。
在有序集合中每個元素代表要給任務,該元素的Score
為隊列的入隊時間戳,任務的Value
為JSON格式,保存了任務的執行情況和業務數據。
Redis驅動下為了實現任務的延遲執行和過期重發,任務將在這三個鍵key
中來回轉移。
Database驅動
Database
驅動是采用數據庫做消息隊列緩存,相比較Redis而言是不推薦。
<?php
/**消息隊列配置*/
use think\Env;
return [
//Database驅動
"connector"=>"Database",//數據庫驅動
"expire"=>60,//任務過期時間,單位為秒,禁用為null
"default"=>"default",//默認隊列名稱
"table"=>"jobs",//存儲消息的表明,不帶前綴
"dsn"=>[],
];
使用數據庫驅動需要創建存放消息的數據表
CREATE TABLE `prefix_jobs` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
`queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '隊列名稱',
`payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '有效負載',
`attempts` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '重試次數',
`reserved` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '訂閱次數',
`reserved_at` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '訂閱時間',
`available_at` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '有效時間',
`created_at` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '創建時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息隊列';
消息和隊列的保存方式
在Database驅動中,每個任務對應到表的一行,queue
字段用來區分不同的隊列,payload
字段保存了消息的執行者和業務數據,payload
字段采用JSON格式的字符串來保存消息。
結構
消息隊列中的角色
- 類名
Command+Worker
的角色為命令行,負責解析命令行參數,控制隊列的啟動和重啟。 - 類名
Queue+Connector
的角色為驅動,負責隊列的創建以及消息的入隊出隊等操作。 - 類型
Job
的角色為任務,負責將消息轉化為一個任務對象,供消費者使用。 - 生產者負責消息的創建與發布
- 消費者負責任務的接收與執行
執行流程
- 命令行
Command
開始監聽隊列queue:work
- 執行進程
Worker
獲取新消息Queue::pop()
- 消息隊列
Queue
返回一個可用的Job
實例$job
3.1 生產者推送Queue::push()
新消息到消息隊列Queue
3.2 消息隊列Queue
返回是否推送成功給生產者 - 執行進程
Worker
調用$job
的fire()
方法 - 消息
Job
解析$job
的payload
,實例化一個消費者,并調用消費者實例的fire($job, $data)
方法。 - 消費者讀取消息內容
$data
,處理業務邏輯,刪除或重發該消息$job->delete()
或$job->release()
。 - 消息
Job
從Database或Redis中刪除消息或重發消息 - 消息
Job
返回消息處理結果給執行進程Worker
- 執行進程
Worker
在終端輸出響應或結束運行
使用流程
- 消息的創建與推送
- 消息的消費與刪除
- 任務發布
- 任務處理
注意:這里會將消息message
與任務job
視為同一概念
消息創建與推送
在業務控制器中創建一個新消息并推送到指定的隊列中
首先創建消息需要引入think\Queue
類
use think\Queue
創建消息時需指定當前消息所歸屬消息隊列的名稱
$job_queue_name = "dismiss_job_queue";
如果是Redis驅動對應的就是List
數據列表的名稱
如果是Database驅動對應的就是prefix_job
表中queue
字段中的內容
創建消息時需要指定當前消息將會由哪個類來負責處理(消費者),當輪到該消息時,系統將生成一個該類的實例,并調用其fire
方法。
$job_handler_classname = "app\index\job\Dismiss";
這里是采用手動指定消息處理類的方式,更合理的做法是事先定義好消息名稱與消費者類名的映射關系,然后根據某個可以獲取該映射關系的類來推送消息,這樣生產者只需要知道消息的名稱,而無需指定具體哪個消費者來處理。
創建消息時需要指定當前任務所需的業務數據,注意數據不能是資源類型resource
,業務數據最終將轉化為json
形式的字符串。
$job_data = [];
$job_data["ts"] = time();
$job_data["bizid"] = uniqid();
$job_data["params"] = $params;
最后將創建的消息推送到消息隊列并等待對應的消費者去執行
$is_pushed = Queue::push($job_handler_classname, $job_data, $job_queue_name);
使用Queue::push
方法將消息推送到消息隊列,其返回值根據驅動不同而不同,如果是Redis驅動則成功返回隨機字符串失敗返回false
,如果是Database驅動則成功返回1
失敗返回false
。
if($is_pushed !== false )
{
echo date("Y-m-d H:i:s")." a new job is pushed to the message queue";
}
else
{
echo date("Y-m-d H:i:s")." a new job pushed fail";
}
例如:在游戲結束后,大廳服務器會發送游戲戰績數據給HTTP接口,接口獲取數據后對其進行加工處理最終得到入庫所需的數據,期間還會涉及到向第三方接口推送數據等等。如果采用同步處理的方式,大廳服務器只有等到所有的處理完畢后才能得到得到結構,由于大廳服務器會根據接口返回的數據判斷當前戰績是否寫入成功,若接口返回數據時間過長,此時服務端將一直處于等待狀態,連接不會被斷開,這種情況對于使用越來越頻繁的接口來說,幾乎是一種噩夢。
完整代碼
<?php
namespace app\api\controller;
use think\Queue;
class Game extends Api
{
public function dismiss(){
//獲取參數
$data = file_get_contents("php://input");
if(empty($data)){
$this->error("post is null");
}
$params = json_decode($data, true);
/*創建新消息并推送到消息隊列*/
// 當前任務由哪個類負責處理
$job_handler_classname = "app\api\job\Dismiss";
// 當前隊列歸屬的隊列名稱
$job_queue_name = "dismiss_job_queue";
// 當前任務所需的業務數據
$job_data = ["ts"=>time(), "bizid"=>uniqid(), "params"=>$params];
// 將任務推送到消息隊列等待對應的消費者去執行
$is_pushed = Queue::push($job_handler_classname, $job_data, $job_queue_name);
if($is_pushed == false){
$this->error("dismiss job queue went wrong");
}
//操作成功
$this->success('success');
}
}
消息的消費與刪除
創建Dismiss
消費者類,用于處理dismiss_job_queue
隊列中的任務。
創建\application\api\job\Dismiss.php
消費者類,并編寫fire()
方法。
<?php
namespace app\api\job;
use think\Log;
use think\queue\Job;
/**
* 消費者類
* 用于處理 dismiss_job_queue 隊列中的任務
* 用于牌局解散
*/
class Dismiss
{
/**
* fire是消息隊列默認調用的方法
* @param Job $job 當前的任務對象
* @param array|mixed $data 發布任務時自定義的數據
*/
public function fire(Job $job, $data)
{
//有效消息到達消費者時可能已經不再需要執行了
if(!$this->checkJob($data)){
$job->delete();
return;
}
//執行業務處理
if($this->doJob($data)){
$job->delete();//任務執行成功后刪除
Log::log("dismiss job has been down and deleted");
}else{
//檢查任務重試次數
if($job->attempts() > 3){
Log::log("dismiss job has been retried more that 3 times");
$job->delete();
}
}
}
/**
* 消息在到達消費者時可能已經不需要執行了
* @param array|mixed $data 發布任務時自定義的數據
* @return boolean 任務執行的結果
*/
private function checkJob($data)
{
$ts = $data["ts"];
$bizid = $data["bizid"];
$params = $data["params"];
return true;
}
/**
* 根據消息中的數據進行實際的業務處理
*/
private function doJob($data)
{
// 實際業務流程處理
return true;
}
}
發布任務
訪問接口/api/game/dismiss
查看推送是否成功
處理任務
切換到當前終端到項目根目錄
$ php think queue:work --queue dismiss_job_queue
實際使用過程中應安裝Supervisor
這樣的通用進程管理工具,它會監控php think queue:work
的進程,一旦失敗會幫助重啟,詳情可參見 《Supervisor》 。
簡單來總結下使用流程
- 安裝Supervisor并編寫應用程序配置腳本,腳本主要用來運行
php think queue:work
命令。 - 運行Supervisor服務,它會讀取主進程和應用程序配置。
- 運行自己編寫的消息隊列并根據日志查看是否正常運行
命令
Work模式 queue:work
用于啟動一個工作進程來處理消息隊列
$ php think queue:work --queue dismiss_job_queue
參數說明
-
--daemon
是否循環執行,如果不加該參數則該命令處理完下一個消息就退出。 -
--queue dismiss_job_queue
要處理的隊列的名稱 -
--delay 0
如果本次任務執行拋出異常且任務未被刪除時,設置其下次執行前延遲多少秒,默認為0。 -
--force
系統處于維護狀態時,是否仍然處理任務,并未找到相關說明。 -
--memory 128
該進程允許使用的內存上限,以M為單位。 -
--sleep 3
如果隊列中無任務則sleep多少秒后重新檢查(work+daemon模式)或退出(listen或非daemon模式) -
--tries 2
如果任務已經超過嘗試次數上限,則觸發“任務嘗試數超限”事件,默認為0。
Daemon模式的執行流程
$ php think queue:work
命令行參數
-
--daemon
是否一直執行 -
--queue
處理的隊列的名稱 -
--delay
重發失敗任務時延遲多少秒才執行 -
--force
系統處于維護狀態時是否仍然處理任務 -
--memory
該進程允許使用的內存上限,以M
為單位。 -
--sleep
入股隊列中無任務則多少秒后重新檢查 -
--tries
任務重發多少次之后進入失敗處理邏輯
如何從緩沖中得到上次重啟的時間?
Cache::get("think:queue:restart")
從緩存得到上次重啟的事件
如何判斷是否退出daemon
循環呢?
- 內存超限:
memory_get_usage()
>=--memory
參數 - 重啟時間有更新:外部通過
php think queue:restart
命令更新了重啟時間的緩存
Listen模式 queue:listen
用于啟動一個listen
進程,然后由listen
進程通過proc_open('php think queue:work --queue="%s" --delay=%s --memory=%s --sleep=%s --tries=%s')
來周期性地創建一次性的work
進程來消費消息隊列,并且限制該work
進程的執行事件,同時通過管道來監聽work
進程的輸出。
$ php think queue:listen --queue dismiss_job_queue
-
--queue dismiss_job_queue
監聽隊列的名稱 -
--delay 0
如果本次任務執行拋出異常且任務未被刪除時,設置其下次執行前延遲多少秒,默認為0。 -
--memory 128
該進程允許使用的內存上限,以M
為單位。 -
--sleep 3
如果隊列中無任務,則多長時間后重新檢查。 -
--tries 0
如果任務已經超過重發次數上限,則進入失敗處理邏輯,默認為0。 -
--timeout 60
工作進程允許執行的最長時間,以秒為單位。
Work模式和Listen模式的異同點
兩者都可以用于處理消息隊列中的任務,區別在于:
- 執行原理不同
Work模式是單進程的處理模式,按照是否設置--daemon
參數又可以分為單次執行和循環執行兩種模式。單次執行不添加--daemon
參數,該模式下Work進程在從處理完下一個消息后直接結束當前進程。當隊列為空時會sleep
一段時間然后退出。循環執行添加了--daemon
參數,該模式下Work進程會循環地處理隊列中的消息直到內存超出參數配置才結束進程。當隊列為空時會在每次循環中sleep
一段時間。
Listen命令是“雙進程+管道”的處理模式,Listen命令所在的進程會循環地創建單次執行模式的Work進程,每次創建的Work進程只消費一個消息就會結束,然后Listen進程再創建一個新的Work進程。Listen進程會定時檢查當前的Work進程執行時間是否超過了--timeout
參數的值,如果已經超過則Listen進程會殺掉所有Work進程,然后拋出異常。Listen進程會通過管道來監聽當前的Work進程的輸出,當Work進程有輸出時Listen進程會將輸出寫入到stdout/stderr
。Listen進程會定時通過proc_get_status()
函數來監控當前的Work進程是否仍再運行,Work進程消費完一個任務之后,Work進程就結束了,其狀態會變成terminated
,此時Listen進程就會重新創建一個新的Work進程并對其計時,新的Work進程開始消費下一個任務。
- 結束時機不同
Listen命令中Listen進程和Work進程會在以下情況下結束:Listen進程會定時檢查當前的Work進程的執行時間是否超過了--timeout
參數的值,如果已經超時此時Listen進程會殺掉當前的Work進程,然后拋出一個ProcessTimeoutException
異常并結束Listen進程。Listen進程會定時檢查自身使用的內存是否超過了--memory
參數的值,如果已經超過此時Listen進程會直接die
掉,Work進程也會自動結束。
- 性能不同
Work命令是在腳本內部做循環,框架腳本在命名執行的初期就已經加載完畢。而Listen模式則是處理完一個任務之后新開一個Work進程,此時會重新加載框架腳本。因此Work模式的性能會比Listen模式高。注意當代碼有更新時Work模式下需要手動去執行php think queue:restart
命令重啟隊列來使改動生效,而Listen模式會自動生效無需其它操作。
- 超時控制能力
Work模式本質上既不能控制進程自身的運行時間,也無法限制執行中的任務的執行時間。舉例來說,假如在某次上線后\app\api\job\Dismiss
消費者的fire
方法中添加一段死循環。
public function fire()
{
while(true){
$consoleOutPut->writeln("looping forever inside a job");
sleep(1);
}
}
這個循環將永遠不能停止,直到任務所在的進程超過內存限制或者由管理員手動結束。這個過程不會由任何的警告。更嚴重的是如果配置了expire
,那么這個死循環的任務可能會污染到同樣處理dismiss_job_queue
隊列的其它Work進程,最后好幾個Work進程將被卡死在這段死循環中。
Work模式下的超時控制能力實際應理解為多個Work進程配合下的過期任務重發能力。
Listen命令可以限制Listen進程創建的Work進程的最大執行時間,Listen命令可以通過--timeout
參數限制Work進程允許運行的最長時間,超過該時間限制后Work進程會被強制殺死,Listen進程本身也會拋出異常并結束。
expire
與timeout
之間的區別
expire
在配置文件中設置,timeout
在Listen命令的命令行參數中設置。expire
和timeout
是兩個不同層次上的概念:expire
是指任務的過期時間,這個時間是全局的影響到所有的Work進程,不管是獨立的Work命令還是Listen模式下創建的Work進程。expire
針對的對象是任務。timeout
是指Work進程的超時時間,這個時間只針對當前執行的Listen命令有效,timeout
針對的對象是Work進程。
- 使用場景不同
Work命令的適用場景是任務數量較多、性能要求較高、任務的執行時間較短、消費者類中不存在死循環/sleep()
/exit()
/die()
等容易導致bug的邏輯。
Listen命令的適用場景是任務數量較少、任務的執行時間較長(如生成大型的Excel報表等)、任務的執行時間需要有嚴格限制。
消息處理流程
消息隊列處理一個任務的具體流程
- 重發超時的任務
超時任務是指任務處于執行中,當前時間 - 任務開始執行的時刻 > expire
時間
重發是指將任務的狀態還原為未執行,并將任務的已執行次數加1。
- 獲取下一個有效任務
有效任務是指未執行、最早可執行的時間 <= 當前時間、按時間先后排序(先進先出)
- 任務次數超限邏輯
任務的已嘗試次數大于命令行中的--tries
參數,命令行中的--tries
參數大于0。
- 觸發次數超限事件
queue_failed
內置的次數超限事件標簽,是否定義了queue_failed
行為,未定義則不處理直接返回,已定義則對次數超限的任務進行處理。
$runHookCb = Behavior::queueFailed() //返回true則刪除任務執行任務失敗回調,返回false則不執行任何操作。
- 消費當前的任務
$job->fire()
從$job
對象的payload
屬性中解析出消費者類,創建消費者類的實例,執行消費者類的實例的fire($job, $data)
方法。
需要在fire($job, $data)
中手動刪除任務,$job
參數表示當前任務對象,$data
參數表示當前的任務數據即創建隊列時傳入的參數。
消息隊列的開始、停止、重啟
開始一個消息隊列
$ php think queue:work
停止所有的消息隊列
$ php think queue:restart
重啟所有的消息隊列
$ php think queue:restart
$ php think queue:work
多模塊多任務的處理
- 多模塊
單模塊項目推薦時間app/job
作為任務類的命名空間,多任務項目可使用app/module/job
作為任務類的命名空間,也可以放在任意可以自動加載到的地方。
- 多任務
如果一個任務類中有多個小任務的話,在發布任務的時候,需要使用任務的“類名@方法名”的形式,例如app\lib\job\Job@task
,注意命令行中的--queue
參數不執行@
的解析。
消息的延遲執行與定時任務
延遲執行是相對于即使執行的,是用來限制某個任務的最早可執行時刻。在到達該時刻之前該任務會被跳過,可以利用該功能實現定時任務。
延遲執行的使用方式
- 在生產者業務代碼中
// 即時執行
$is_pushed = Queue::push($job_handler_classname, $job_data, $job_queue_name)
// 延遲2秒執行
$is_pushed = Queue::later(2, $job_handler_classname, $job_data_arr, $job_queue_name);
// 延遲到2019-06-01 00:00:00時刻執行
$time2wait = strtotime("2019-06-01 00:00:00") - strtotime("now");
$is_pushed = Queue::later($time2wait, $job_handler_classname, $job_data_arr, $job_queue_name);
- 在消費者類中
// 重發,即時執行
$job->release();
// 重發,延遲2秒后執行
$job->release(2);
// 延遲到2019-06-01 00:00:00時刻執行
$time2wait = strtotime("2019-06-01 00:00:00") - strtotime("now");
$job->release($time2wait);
- 在命令行中
如果消費者類的fire
方法拋出了異常且任務未被刪除時,將自動重發該任務。重發時會設置下次執行前延遲多少秒,默認為0。
$ php think queue:work --delay 3
消息重發
消息重發時機有三種情況:
- 在消費者類中手動重發
if($is_job_done === false)
{
$job->release();
}
Work進程自動重發需要同時滿足以下兩個條件:消費者類的
fire()
方法拋出異常、任務未被刪除當配置了
expire
不為null
時,Work進程內部每次查詢可用任務之前會自動重發已過期的任務。
注意:在Database模式下,2.7.1和2.7.2中的重發邏輯是先刪除原來的任務,然后插入一個新的任務。2.7.3中的重發機制是直接更新原任務。在Redis模式下,三種重發都是先刪除再插入。不管是那種重發方式,重發之后任務的已嘗試次數會在原來的基礎上加1。
此外,消費者類中需要注意,如果fire()
方法中拋出異常,將出現兩種情況:
- 如果不需要自動重發的話,請在拋出異常之前將任務刪除
$job->delete()
,否則會被框架自動重發。 - 如果需要自動重發的話,請直接拋出異常,不要在
fire()
方法中手動使用$job->release()
,這樣將導致任務被重發兩次,產生兩個一樣的新任務。
Redis驅動下的任務重發細節
在Redis驅動下為了實現任務的延遲執行和過期重發,任務將在這三個key
中來回轉移。
在Database模式下消息處理的消息流程中,如果配置的expire
不是null
那么think-queue
的work
進程每次在獲取下一個可執行任務之前,會先嘗試重發所有過期的任務。而在Redis驅動下這個步驟則做了更多的事情,詳情如下:
- 從
queue:xxx:delayed
的key
中查詢出有哪些任務在當前時刻已經可以開始執行,然后將這些任務轉移到queue:xxx
的key
的尾部。 - 從
queue:xxx:reserved
的key
中查詢出有哪些任務在當前時刻已經過期,然后將這些任務轉移到queue:xxx
的key
的尾部。 - 嘗試從
queue:xxx
的key
的頭部取出一個任務,如果取出成功,則將這個任務轉移到queue:xxx:reserved
的key
的頭部,同時將這個任務實例化成任務對象,交給消費者去執行。
Redis隊列中的過期任務重發步驟,執行前:
Redis隊列中的過期任務重發步驟,執行后:
任務的失敗回調與警告
當同時滿足以下條件時將觸發任務失敗回調:
- 命令行的
--tries
參數的值大于0 - 任務的已嘗試次數大于命令行的
--tries
參數 - 開發者添加了
queue_failed
事件標簽及其對應的回調代碼 - 消費者類中定義了
failed()
方法用于接收任務失敗的通知
注意,queue_failed
標簽需要在安裝think-queue
之后手動去/app/tags.php
文件中添加。
注意事項
-任務完成后使用$job->delete()
刪除任務
- 在消費者類的
fire()
方法中使用$job->attempt()
檢查任務已執行次數,對于次數異常的做相應的處理。 - 在消費者類的
fire()
方法在中根據業務數據來判斷該任務是否已經執行過,以避免該任務被重復執行。 - 編寫失敗回調事件將事件中失敗的任務及時通知給開發人員
拓展
- 隊列的穩定性和拓展性
穩定性:不管是listen
模式還是work
模式,建議使用supervisor
或自定義的cron
腳本去定時檢查work
進程是否正常。
拓展性:當某個隊列的消費者不足時在給這個隊列添加work
進程即可
使用注意
最好配置本地的Redis,使用遠程Redis曾出現無法解釋的原因。
$ yum install -g redis
$ systemctl restart redis
停止原正在運行的
$ supervisorctl shutdown
重新加載服務
$ supervisord -c /etc/supervisord.conf
$ supervisorctl reload
$ ps aux | grep supervisord
$ top
未完待續...