簡介
目標: 使用PHP和Redis提供使用簡單而功能強大的消息隊列系統.
安裝
目前內嵌到Chip框架, 也可以獨立使用, 經過簡單的修改可與其他核心模塊完全解耦.
概述
使用說明
配置文件
在Chip裏的conf目錄下創建queue.ini, 根據自己的機器增加以下配置項:
[product]
[dev : product]
queue.redis.0.host = 127.0.0.1 ; 機器IP
queue.redis.0.port = 6379 ; 機器端口
queue.redis.0.db = 0 ; redis 數據庫
queue.redis.0.name = queue ; 隊列名
queue.redis.0.distributed = 0 ; 是否開啓併發模式
queue.redis.1.host = 127.0.0.1
queue.redis.1.port = 6379
queue.redis.1.db = 0
queue.redis.1.name = queue
queue.redis.1.distributed = 0
[test : dev]
創建隊列
$queue = \Chip\Queue::getInstance();
自定義配置創建
如果不想使用默認配置文件裏的配置,可以自定義配置文件和配置索引
$queue = \Chip\Queue::getInstance('myself.ini', 'mytest');
創建任務
創建任務是將一個消息寫入一個有名字的管道, 下面管道名爲”mytube”
$data = array(
'name' => 'kevin',
'content' => array(
'test' => 'some string',
'num' => null,
),
);
$queue = Queue::getInstance();
$queue->putInTube('mytube', $data);
創建延時任務
延時任務是在入隊列後, 不會馬上被處理進程獲取, 在延遲指定時間後纔會被處理.
// 3600秒後纔會被處理
$option = array(
'delay' => 3600, // 單位爲秒
);
$queue->putInTube('mytube', $data, $option);
創建定時任務
定時任務入隊列後, 在特定時間點纔會被處理進程獲取並處理.
// 在2015-10-29 23:34任務纔會被處理
$option = array(
'timing' => '2015-10-29 23:34',
);
$queue->putInTube('mytube', $data, $option);
定時參數的格式遵循PHP的日期和時間格式 PHP date and time formats, 可以使用:
Next Monday
+1 days
last day of next month
2013-09-13 00:00:00
- and so on..
注意的是, 如果創建了一個過去時間的定時任務, 任務不會被丟棄, 而是會馬上被觸發.
創建週期任務
週期任務可以代替非具體時間的週期性的crontab.
// 每3600秒被觸發一次,不會銷燬.
$option = array(
'periodic' => 3600,
);
$queue->putInTube('mytube', $data, $option);
創建失敗重試任務
處理的時候如果處理失敗後, 可以設定一個重試次數, 來重複嘗試處理這個任務:
這個參數可以與上面的時間控制的參數一起使用.
$option = array(
'attempts' => 5,
);
$queue->putInTube('mytube', $data, $option);
設定處理時長TTR
每個消息任務都有規定的最大運行時間, 默認是5分鐘, 超時後會被視爲失敗的任務.
可以在創建任務的時候指定處理最大時長:
這個參數可以與上面的時間控制的參數一起使用.
$option = array(
'ttr' => 600, // 最大處理時長爲10分鐘
);
$queue->putInTube('mytube', $data, $option);
處理任務的時候, 如果時間過長, 也可以主動聲明處理時間:
set_time_limit(600);
或者使用touch來重新計算處理時間:(參考處理任務)
$job->touch();
處理任務
處理消息任務需要創建一個守護進程的腳本, 必須在CLI模式下執行, 這裏可以使用Chip裏的CLI-TASK:
class DaemonTask extends TaskBase
{
/**
* 創建守護進程, 需要在CLI模式下執行
*/
public function indexAction()
{
$queue = Queue::getInstance();
// 創建進程, 監聽'mytube'這個管道, 處理隊列任務
$queue->doWork('mytube', function(Caster $job) {
$data = $job->getBody();
// process
// 特別注意的一點, 匿名函數裏的程序是以子進程形式存在的
// 如果正常處理完一個JOB後, 需要發送結束信號: exit(0)
exit(0);
});
}
}
任務狀態調度控制
消息的狀態有四種: 準備,訂閱,延遲,失敗, 這些狀態的生命週期如下所示:
put with delay release with delay
----------------> [DELAYED] <------------.
| |
| (time passes) |
| |
put v reserve | delete
-----------------> [READY] ---------> [RESERVED] --------> *poof*
^ ^ | |
| \ release | |
| `-------------' |
| |
| kick |
| |
| bury |
[FAILED] <---------------'
|
| delete
`--------> *poof*
處理進程中的任務處在訂閱狀態(RESERVED), 一般處理完後會自動delete, 有異常的話會bury到失敗隊列.
開發者也可以主動delete,release,或者bury這個任務:
$queue->doWork('mytube', function(Caster $job) {
$data = $job->getBody();
if ($data['name'] != 'kevin') {
$job->bury();
} else {
$job->release(120); // 將任務回置爲延遲任務
}
exit(0);
});
處理失敗狀態的任務:
$queue = Queue::getInstance();
$ids = $queue->getIdsByState(Caster::STATE_FAILED, 'mytube');
foreach ($ids as $id) {
$job = Caster::reload($id);
$job->kick(); // 將失敗的任務重新放置到準備隊列裏
}
併發模式
多數情況,消息隊列保持”FIFO”(先進先出)的原則, 然而也有業務會用到分佈式場景.
分佈式處理消息會提高處理效率,爲了避免因併發讀取而出現的問題,需要將配置文件中distributed 設置爲1
...
queue.redis.1.distributed = 1
設計理念
利用Redis多種數據類型的特點,將每條隊列構建成如下的數據結構:
array(
'id' => null,
'tube' => null,
'data' => array(),
'state' => '', // 任務狀態
'created_at' => '',
'updated_at' => '',
'error' => '',
'failed_at' => '',
'timing' => 0, // 定時, 單位毫秒
'attempts' => 0, // 嘗試次數計數器
'max_attempts' => 3, // 最大嘗試次數限制
'retry_times' => 0, // 重新處理過的次數
'periodic' => 0, // 週期任務生命週期時長, 單位秒
'ttr' => 30, // 進程運行生命週期時長, 單位秒
);
每條任務會生成對應的Redis中Set、SortSet、Hash等數據類型用以流程控制:
queue:job:[id] -> key, value 哈希存儲一個任務的業務數據
queue:tubes -> tube 集合存儲所有tube類型
queue:jobs -> id 有序集合存儲整個任務列表的次序
queue:jobs:[state] -> id 有序集合存儲特定狀態任務列表的次序
queue:jobs:[tube]:[state] -> id 有序集合存儲一個tube下的特定狀態的任務列表的次序