[TOC]
延遲佇列和非同步佇列說明
- 1、有效期:限時活動、拼團…
- 2、超時處理:取消超時未支付訂單、超時自動確認收貨…
- 3、延遲處理:機器人點贊/觀看數/評論/關注、等待依賴條件…
- 4、重試:網路異常重試、叫車派單、依賴條件未滿足重試…
- 5、定時任務:智慧裝置定時啟動…
- 延遲
- 佇列(其實不能稱為佇列,更貼切的應該是不重複的元素集)
- 延遲任務
- 資料量大
- 實時性要求高
- 時間輪
- RabbitMQ死性佇列
- 基於redis的zset實現
專案 | 比較 |
---|---|
複雜性 | redis < RabbitMQ < 時間輪 |
安全性 | redis < RabbitMQ <= 時間輪 |
效能 | redis > 時間輪 > RabbitMQ |
鑑於redis實現延遲佇列是一種理解起來較為直觀,可以快速落地的方案,使用 Redis 叢集來支援高併發和高可用,並且可以依賴redis自身持久化可以實現安全性保證,是一種不錯的延遲佇列的實現方案,以下實現方案研究均基於redis
基於key的過期監聽
實現思路:
1.起一個執行緒,開啟對任務key(如:未支付訂單單號)的watch
;
2.一旦key過期(訂單未支付超時時間),觸發event
進行業務邏輯處理(取消訂單)。
注:這種方法處理起來簡單,但是在資料量較大的情況下,這種方案對效能影響較大基於zset集合
實現思路:
1.利用zadd
向集合中插入元素,以元素(訂單)的時間戳(超時時間)作為score
2.利用zrangebyscore
以【0-當前時間戳】進行獲取需要處理的元素(即為超時待處理訂單)
注:這種方法相較於上面方法,實現較為複雜,但是對效能保證較好,因此是應用最廣泛的延遲佇列實現方案
下面是基於redis的zset實現延遲佇列的具體示例
生產者
/**
* 生產
*/
public function producer()
{
$orderMap = [
'orderNo.1' => time() + 10,//訂單1需要10s之後進行消費處理
'orderNo.2' => time() + 100,//訂單2需要100s之後進行消費處理
'orderNo.3' => time() + 1000,//訂單3需要1000s之後進行消費處理
];
Redis::connection()->zadd(self::$delayKey, $orderMap);
}
生產者:
- 利用
zadd
,以延遲時間戳作為score,元素唯一標誌(訂單號不重複)作為value,向需要處理的key中插入元素即可注意:因為是集合,因此需要保證集合元素的唯一性(score可以相同)
完成生產者,那麼如何消費呢?常見的消費有以下幾種
消費者
示例1
/**
* 消費
*/
public function consumer1()
{
/**
* 迴圈執行消費
*/
while (true) {
$msg = Redis::connection()->zrangebyscore(self::$delayKey, 0, time(), ['limit' => ['offset' => 0, 'count' => 1]]);
if ($msg) {
// 刪除當前處理訊息
if (Redis::connection()->zrem(self::$delayKey, $msg)) {
// TODO 業務處理並返回處理結果
}
}
}
}
示例1中:
1.利用zrangebyscore
按照score(此處為元素對應超時時間戳)進行獲取滿足條件的集合元素(超時待取消訂單);
2.當有滿足的條件的元素(超時待取消訂單),先刪除該元素(保證不被其他程式取到),再進行業務邏輯處理;
3.此處為了考慮效能,建議每次只處理1(count=1)條訊息資料,offset=0,
思考:
在常規處理中,該實現已經能夠滿足業務需求,
- 但是這種實現有沒有什麼問題?
when延遲佇列滿足條件的元素為空(或者集合為空)時候:
程式會頻繁不斷向redis服務獲取滿足條件元素,這樣會造成redis服務資源佔用和浪費,- 怎麼辦呢?
可以在沒有取到滿足的條件時候讓程式阻塞一段時間unsleep(100000);
這種方法實際上就是用時間換取資源,注意控制阻塞時間長短,不宜太短,也不宜太長(影響即時性),完整程式碼見示例2
示例2
/**
* 消費
*/
public function consumer2()
{
/**
* 迴圈執行消費
*/
while (true) {
$msg = Redis::connection()->zrangebyscore(self::$delayKey, 0, time() + 1500, ['limit' => ['offset' => 0, 'count' => 1]]);
if ($msg) {
// 刪除當前處理訊息
if (Redis::connection()->zrem(self::$delayKey, $msg)) {
// TODO 業務處理並返回處理結果
}
} else {
/**
* 阻塞1s,減少redis服務資源浪費
*/
unsleep(100000);
}
}
}
示例2:
思考:
這種實現看起來已經完全沒有問題(單例部署架構中)
- 在分散式架構中呢?
有可能出現zrangebyscore
和zrem
非同一個客戶端的問題,即原子性問題- 怎麼辦呢?
採用lua指令碼解決,見示例3
示例3
/**
* 消費
* 沒有備份機制的lua指令碼
*/
public function consumer3()
{
$luaScript = <<<EOF
local delayKey = KEYS[1]
local start = ARGV[1]
local endTime = ARGV[2]
local limitNum = ARGV[3]
local result = redis.call('zrangebyscore',delayKey,start,endTime,'limit',0, limitNum)
if next(result) ~= nil
then
local res = redis.call('zrem',delayKey,unpack(result))
if res > 0
then
return result
end
else
return {}
end
EOF;
/**
* 迴圈執行消費
*/
while (true) {
$startTime = 0;
$endTime = time();
$limit = 1;
/**
* 為了防止併發,保證原子性操作
* 利用lua指令碼獲取滿足條件的訊息,並刪除滿足條件訊息
* 返回滿足條件的訊息
*/
$msg = Redis::connection()->eval($luaScript, 1, self::$delayKey, $startTime, $endTime + 9999999, $limit);
if ($msg) {
// TODO 業務處理並返回處理結果
} else {
/**
* 阻塞1s,減少redis服務資源浪費
*/
unsleep(100000);
}
}
}
總結:方案3中解決上面存在的問題,是一種最優解
本作品採用《CC 協議》,轉載必須註明作者和本文連結