Redis 隊列

隊列的實現

舉例:

隊列主要用在系統解耦、流量削峰、異步處理、數據順序處理等場景。新手在使用時可能會犯一些常見的錯誤。下面講一個新手容易犯的錯誤,在這個示例中把隊列的入隊、出隊和Redis存儲節點的主從關係給混淆了,示例如下

存儲:

Redis主節點M, 使用數據List類型做為隊列,列表名稱M (標記為M.L,意為主節點上的List)

Redis從節點S, 做為Redis主節點的備,有和主節點相同的列表名稱L (標記為S.L,意為從節點上的List

應用程序:

應用名稱App

操作:

入隊命令: LPUSH M.L Data

出隊命令: RPOP S.L

我們知道,Redis主從節點數據的流向是主節點->從節點,隊列中一般數據也是隊尾(入隊)-> 隊頭(出隊),這兩個數據流向混淆後就會出現以上的錯誤。

Redis 隊列

image.png

上圖顯然不是我們想要的結果,這種設計導致的問題是Redis主節點使用的內存會不斷增長直至觸發Redis的LRU策略導致數據丟失或者無法入隊。Redis的從節點S僅僅是做為主節點M的一個備份節點存在的。

Redis 隊列

image.png

阻塞隊列

阻塞隊列是一種特殊的隊列,具體是指對出隊動作在隊列為空時的阻塞行為以及在有元素入隊後對出隊的通知行為.我們知道事件通知機制是服務端通過一定的途徑向客戶端發送事件消息來實現的,服務端和客戶端通常保持長鏈接。生產者(通知方)向隊列中發送事件消息,消費者(接收方)從隊列中拿走(POP)事件消息,當隊列中沒有事件消息的時候,消費者(接收方)阻塞,消費者(接收方)和隊列之間保持長鏈接。

消費者(接收方)的代碼(Java)邏輯應該大致如下:

 while (true){ try { EventMessage em = client.brpop(list, timeout); doSomething(em); }catch(Exception e){ continue; } }

使用循環是因為每次brpop命令只能拿到一個隊列中的元素,為了得到消息的連續性必須循環brpop。在循環中使用try...catch...是防止在brpop的過程中由於網絡閃斷、連接池等因素導致的連接不可用拋出異常致使循環不可繼續不能連續獲取事件消息。

生產者(通知方)的Redis命令邏輯大致如下:

MULTI some command... LPUSH list x EXEC

阻塞隊列與PUB/SUB在實現事件通知上的區別

  • 1.PUB/SUB方式在消息消費者(被通知方)訂閱一個或多個話題後和Redis保持長鏈接等待通知,之後可以連續的得到事件通知消息;阻塞隊列brpop/blpop方式每次只能獲取一個事件消息(即便pop多個隊列也是一樣),客戶端需要循環使用brpop/blpop來獲取事件消息,直至隊列為空阻塞。

  • 2.PUB/SUB方式實現的通知只有在消息消費者(被通知方)和Redis服務有在線連接的情況下才能收到,一旦連接斷開消費者(被通知方)將不能收到通知,即便重新成功連接也無法收取丟失的通知消息;而阻塞隊列在客戶端斷開後重新連接成功後可以收到通知消息。

  • 3.PUB/SUB方式事件消息消費者可以有多個並且每個消費者都能得到相同的消息;而阻塞隊列雖然事件消費者可以有多個但是消息只是分發給其中一個消費者,消息無法重複消費。

可靠隊列

在Redis的列表(List)實現的隊列中,一般一個客戶端通過LPUSH命令將消息放入隊列中,而另一個客戶端通過RPOP/BRPOP 命令有順序的取出隊列中的消息進行消費。但是這種方式卻不一定安全,因為在這個過程中,一個客戶端可能在取出一個消息之後在處理這個消息之前崩潰,而未處理完的消息也就因此而丟失,並且無法找回。在一些希望是更可靠的消息傳遞系統中的應用上,這可能會導致一些問題。使用 RPOPLPUSH/BRPOPLPUSH 命令可以解決這個問題:因為它不僅返回一個消息,同時還將這個消息添加到另一個備份列表當中,當一個客戶端完成某個消息的處理之後,可以從備份表中把消息刪除。

Redis 隊列

image.png

對於備份列表一般有兩種設計方式:每個客戶端各自私有一個備份列表和所有客戶端公用一個備份列表。兩種方式實現機制不同,舉例如下

  1. 每個客戶端各自私有一個備份隊列

  • 客戶端從隊列裡獲取消息之前首先檢查自己的私有備份隊列。

  • 如果備份隊列中有消息說明這條消息沒有處理完,使用RPOPLPUSH/BRPOPLPUSH把這個消息取出來的同時再次放入備份隊列(再次放入是防止從備份隊列中取出後沒處理完丟失),處理消息完成後把備份隊列中的消息刪除。

  • 如果備份隊列中沒有消息,從消息隊列中使用RPOPLPUSH/BRPOPLPUSH獲取消息並把消息放入備份隊列,處理完消息後把備份隊列中的消息刪除。

按照規則形成的處理邏輯,對每一個客戶端分別使用自己私有的備份隊列從根本上規避了所有客戶端公用備份隊列中由於“後發先至”等問題導致的複雜邏輯。

Redis 隊列

image.png

  • 2.所有客戶端公用一個備份隊列,專門客戶端做確認檢查

  • 所有客戶端從隊列中取出消息數據並通過RPOPLPUSH/BRPOPLPUSH放入備份隊列

  • 所有客戶端處理消息完成後再從隊列中取出下一個消息重複以上動作

  • 專門客戶端處理備份消息隊列校對消息是否做完,如果做完則刪除,否則重做消息。專門客戶端起到一個補漏的作用。

Redis 隊列

image.png

旋轉隊列

在使用RPOPLPUSH命令的時候,它的兩個參數如果是相同的隊列鍵,客戶端就可以一個接一個的獲取從隊頭到隊尾的所有元素並且把獲取的元素放置到隊尾。我們稱作隊列的旋轉。這種情況在多個客戶端對同一個隊列進行旋轉時也有效。甚至有客戶端在旋轉時增加隊列的元素也是可行的。

此種模型對於隊列和客戶端來說都是易於擴展的。因為即便客戶端獲取元素後失敗旋轉到下一個週期後仍能獲取到元素因此是安全的。這個模型使得我們可以很容易實現這樣一類系統:有多個客戶端,需要連續不斷地對一些元素進行週期性的遍歷輪訓處理。一個典型的例子就是數據定時採集系統:採集點保存在隊列中,客戶端從隊列中拿採集點然後採集數據。

Redis 隊列

image.png

優先權隊列

優先隊列和一般隊列有所不同,它不完全遵循先進先出的規則,而是根據隊列中元素的優先權,優先權最大的先被取出。隊列中的元素不僅存儲元素本身還存儲元素的優先權數據。使用Redis數據結構實現的方式是按照優先權建隊列(列表),相同優先權的元素在同一個隊列中,客戶端在使用BRPOP/RPOP命令使隊列中的元素出隊的時候參數按照優先權從高到低的順序進行。

例如,

  • 隊列1:包含0~9,10個元素

  • 隊列2:包含0~3,4個元素

  • 隊列3:包含0~5,6個元素

優先權:隊列1>隊列2>隊列3

客戶端使用命令為:RPOP/BRPOP 隊列1 隊列2 隊列3

Redis 隊列

image.png


個人介紹:

高廣超:多年一線互聯網研發與架構設計經驗,擅長設計與落地高可用、高性能、可擴展的互聯網架構。

本文首發在 高廣超的簡書博客 (http://www.jianshu.com/p/5d98222e36c0)轉載請註明!


分享到:


相關文章: