think-queue

參考資料

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參數用于指定任務的過期時間,單位為秒。那么什么是過期任務呢?過期任務是任務的狀態為執行中,任務的開始時刻 + 過期時間 > 當前時刻。

  • expirenull時表示不會檢查過期的任務,執行超時的任務會一直留在消息隊列中,需要開發者另行處理(刪除或重發),因此性能相對較高。
  • expire不為null則表示會在每次獲取下一個任務之前檢查并重發過期(執行超時)的任務。

消息與隊列的保存方式

Redis中消息隊列名稱

在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的角色為任務,負責將消息轉化為一個任務對象,供消費者使用。
  • 生產者負責消息的創建與發布
  • 消費者負責任務的接收與執行
類關系

執行流程

  1. 命令行Command開始監聽隊列queue:work
  2. 執行進程Worker獲取新消息Queue::pop()
  3. 消息隊列Queue返回一個可用的Job實例$job
    3.1 生產者推送Queue::push()新消息到消息隊列Queue
    3.2 消息隊列Queue返回是否推送成功給生產者
  4. 執行進程Worker調用$jobfire()方法
  5. 消息Job解析$jobpayload,實例化一個消費者,并調用消費者實例的fire($job, $data)方法。
  6. 消費者讀取消息內容$data,處理業務邏輯,刪除或重發該消息 $job->delete()$job->release()
  7. 消息Job從Database或Redis中刪除消息或重發消息
  8. 消息Job返回消息處理結果給執行進程Worker
  9. 執行進程Worker在終端輸出響應或結束運行

使用流程

  1. 消息的創建與推送
  2. 消息的消費與刪除
  3. 任務發布
  4. 任務處理

注意:這里會將消息message與任務job視為同一概念

消息創建與推送

在業務控制器中創建一個新消息并推送到指定的隊列中

首先創建消息需要引入think\Queue

use think\Queue

創建消息時需指定當前消息所歸屬消息隊列的名稱

$job_queue_name = "dismiss_job_queue";

如果是Redis驅動對應的就是List數據列表的名稱

Redis中消息隊列名稱

如果是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》

簡單來總結下使用流程

  1. 安裝Supervisor并編寫應用程序配置腳本,腳本主要用來運行php think queue:work命令。
  2. 運行Supervisor服務,它會讀取主進程和應用程序配置。
  3. 運行自己編寫的消息隊列并根據日志查看是否正常運行

命令

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模式的執行流程

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進程本身也會拋出異常并結束。

expiretimeout之間的區別

expire在配置文件中設置,timeout在Listen命令的命令行參數中設置。expiretimeout是兩個不同層次上的概念: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-queuework進程每次在獲取下一個可執行任務之前,會先嘗試重發所有過期的任務。而在Redis驅動下這個步驟則做了更多的事情,詳情如下:

  1. queue:xxx:delayedkey中查詢出有哪些任務在當前時刻已經可以開始執行,然后將這些任務轉移到queue:xxxkey的尾部。
  2. queue:xxx:reservedkey中查詢出有哪些任務在當前時刻已經過期,然后將這些任務轉移到queue:xxxkey的尾部。
  3. 嘗試從queue:xxxkey的頭部取出一個任務,如果取出成功,則將這個任務轉移到queue:xxx:reservedkey的頭部,同時將這個任務實例化成任務對象,交給消費者去執行。

Redis隊列中的過期任務重發步驟,執行前:

Redis隊列中的過期任務重發步驟,執行前

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

未完待續...

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容

  • 依賴 redis-server redis php擴展 參考此教程 安裝 composer 最好通過compose...
    hfm0922閱讀 3,104評論 0 1
  • 傳統的程序執行流程一般是 即時|同步|串行的,在某些場景下,會存在并發低,吞吐量低,響應時間長等問題。在大型系統中...
    紅塵一落君莫笑閱讀 15,100評論 3 4
  • 轉載00天火00文章保存一下先。。 說明 好像是tp3.2的bug,自帶的redis驅動不是那么好用。。。找了方法...
    geeooooz閱讀 2,738評論 0 2
  • 1.1 資料 ,最好的入門小冊子,可以先于一切文檔之前看,免費。 作者Antirez的博客,Antirez維護的R...
    JefferyLcm閱讀 17,083評論 1 51
  • 原帖地址:http://www.lxweimin.com/p/2f14bc570563 redis概述 Redis...
    onlyHalfSoul閱讀 2,180評論 0 28