1. 程式人生 > >Redis 非同步訊息佇列與延時佇列

Redis 非同步訊息佇列與延時佇列

        訊息中介軟體,大家都會想到  Rabbitmq 和 Kafka 作為訊息佇列中介軟體,來給應用程式之間增加非同步訊息傳遞功能。這兩個中介軟體都是專業的訊息佇列中介軟體,特性之多超出了大多數人的理解能力。但是這種屬於重量級的應用,使用比較麻煩點。如果是輕量級的,使用 Redis就可以。比如對於那些只有一組消費者的訊息佇列,使用 Redis 就可以非常輕鬆的搞定。Redis 的訊息佇列不是專業的訊息佇列,它沒有非常多的高階特性,沒有 ack 保證,如果對訊息的可靠性沒有極致的要求,那麼它可以拿來使用。
 

非同步訊息佇列

Redis 的 list(列表) 資料結構常用來作為非同步訊息佇列使用,使用rpush/lpush操作入佇列,使用lpop 和 rpop來出佇列。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,就完美解決了上面的問題。

問題喋喋不休:

空閒連線自動斷開 你以為上面的方案真的很完美麼?先別急著開心,其實他還有個問題需要解決。 什麼問題?—— 空閒連線的問題。 如果執行緒一直阻塞在哪裡,Redis 的客戶端連線就成了閒置連線,閒置過久,伺服器一般會主動斷開連線,減少閒置資源佔用。這個時候blpop/brpop會丟擲異常來。 所以編寫客戶端消費者的時候要小心,注意捕獲異常,還要重試。

訊息延時佇列

        延時佇列可以通過 Redis 的 zset(有序列表) 來實現。我們將訊息序列化成一個字串作為 zset 的value,這個訊息的到期處理時間作為score,然後用多個執行緒輪詢 zset 獲取到期的任務進行處理,多個執行緒是為了保障可用性,萬一掛了一個執行緒還有其它執行緒可以繼續處理。因為有多個執行緒,所以需要考慮併發爭搶任務,確保任務不能被多次執行。 Redis 的 zrem 方法是多執行緒多程序爭搶任務的關鍵,它的返回值決定了當前例項有沒有搶到任務,因為 loop 方法可能會被多個執行緒、多個程序呼叫,同一個任務可能會被多個程序執行緒搶到,通過 zrem 來決定唯一的屬主。 同時,我們要注意一定要對 handle_msg 進行異常捕獲,避免因為個別任務處理問題導致迴圈異常退出。 

問題來了:

同一個任務可能會被多個程序取到之後再使用 zrem 進行爭搶,那些沒搶到的程序都是白取了一次任務,這是浪費。解決辦法:Lua是Redis內建指令碼,執行Lua指令碼時,Redis執行緒會依次執行指令碼中的語句,對於客戶端來說操作是原子性的,將 zrangebyscore 和 zrem 一同挪到伺服器端進行原子化操作,這樣多個程序之間爭搶任務時就不會出現這種浪費了。