瞭解一下Redis佇列【緩兵之計-延時佇列】

OldBoy~發表於2018-02-19

我們平時習慣於使用 Rabbitmq 和 Kafka 作為訊息佇列中介軟體,來給應用程式之間增加 非同步訊息傳遞功能。這兩個中介軟體都是專業的訊息佇列中介軟體,特性之多超出了大多數人的理 解能力。

使用過 Rabbitmq 的同學知道它使用起來有多複雜,發訊息之前要建立 Exchange,再創 建 Queue,還要將 Queue 和 Exchange 通過某種規則繫結起來,發訊息的時候要指定 routing- key,還要控制頭部資訊。消費者在消費訊息之前也要進行上面一系列的繁瑣過程。但是絕大 多數情況下,雖然我們的訊息佇列只有一組消費者,但還是需要經歷上面這些繁瑣的過程。

有了 Redis,它就可以讓我們解脫出來,對於那些只有一組消費者的訊息佇列,使用 Redis就可以非常輕鬆的搞定。Redis 的訊息佇列不是專業的訊息佇列,它沒有非常多的高階特性, 沒有 ack 保證,如果對訊息的可靠性有著極致的追求,那麼它就不適合使用。

非同步訊息佇列

Redis 的 list(列表) 資料結構常用來作為非同步訊息佇列使用,使用rpush/lpush操作入佇列, 使用 lpop 和 rpop 來出佇列。

> rpush notify-queue apple banana pear (integer) 3
> llen notify-queue
(integer) 3
> lpop notify-queue
"apple"
> llen notify-queue
 (integer) 2
> lpop notify-queue
 "banana"
> llen notify-queue
 (integer) 1
> lpop notify-queue
 "pear"
> llen notify-queue
 (integer) 0
 > lpop notify-queue
 (nil)

上面是 rpush 和 lpop 結合使用的例子。還可以使用 lpush 和 rpop 結合使用,效果是一 樣的。這裡不再贅述。

佇列空了怎麼辦?

客戶端是通過佇列的 pop 操作來獲取訊息,然後進行處理。處理完了再接著獲取訊息, 再進行處理。如此迴圈往復,這便是作為佇列消費者的客戶端的生命週期。

可是如果佇列空了,客戶端就會陷入 pop 的死迴圈,不停地 pop,沒有資料,接著再 pop, 又沒有資料。這就是浪費生命的空輪詢。空輪詢不但拉高了客戶端的 CPU,redis 的 QPS 也 會被拉高,如果這樣空輪詢的客戶端有幾十來個,Redis 的慢查詢可能會顯著增多。

通常我們使用 sleep 來解決這個問題,讓執行緒睡一會,睡個 1s 鍾就可以了。不但客戶端 的 CPU 能降下來,Redis 的 QPS 也降下來了。

佇列延遲

用上面睡眠的辦法可以解決問題。但是有個小問題,那就是睡眠會導致訊息的延遲增大。 如果只有 1 個消費者,那麼這個延遲就是 1s。如果有多個消費者,這個延遲會有所下降,因 為每個消費者的睡覺時間是岔開來的。

有沒有什麼辦法能顯著降低延遲呢?你當然可以很快想到:那就把睡覺的時間縮短點。這 種方式當然可以,不過有沒有更好的解決方案呢?當然也有,那就是 blpop/brpop。

這兩個指令的字首字元 b 代表的是 blocking,也就是阻塞讀。

阻塞讀在佇列沒有資料的時候,會立即進入休眠狀態,一旦資料到來,則立刻醒過來。消 息的延遲幾乎為零。用 blpop/brpop 替代前面的 lpop/rpop,就完美解決了上面的問題。.

空閒連線自動斷開

你以為上面的方案真的很完美麼?先別急著開心,其實他還有個問題需要解決。 什麼問題?—— 空閒連線的問題。

鎖衝突處理

上節課我們講了分散式鎖的問題,但是沒有提到客戶端在處理請求時加鎖沒加成功怎麼辦。 一般有 3 種策略來處理加鎖失敗:

如果執行緒一直阻塞在哪裡,Redis 的客戶端連線就成了閒置連線,閒置過久,伺服器一般

會主動斷開連線,減少閒置資源佔用。這個時候 blpop/brpop 會丟擲異常來。 所以編寫客戶端消費者的時候要小心,注意捕獲異常,還要重試。...

1、直接丟擲異常,通知使用者稍後重試

這種方式比較適合由使用者直接發起的請求,使用者看到錯誤對話方塊後,會先閱讀對話方塊的內 容,再點選重試,這樣就可以起到人工延時的效果。如果考慮到使用者體驗,可以由前端的程式碼 替代使用者自己來進行延時重試控制。它本質上是對當前請求的放棄,由使用者決定是否重新發起 新的請求。

2、sleep 一會再重試

sleep 會阻塞當前的訊息處理執行緒,會導致佇列的後續訊息處理出現延遲。如果碰撞的比 較頻繁或者佇列裡訊息比較多,sleep 可能並不合適。如果因為個別死鎖的 key 導致加鎖不成 功,執行緒會徹底堵死,導致後續訊息永遠得不到及時處理。

3、將請求轉移至延時佇列,過一會再試;

這種方式比較適合非同步訊息處理,將當前衝突的請求扔到另一個佇列延後處理以避開衝突。

 

摘自《Redis深度歷險:核心原理和應用時間》 

相關文章