把Redis當作佇列來用,真的合適嗎?
「來源: |架構師修行之路 ID:jiagoushixiuxing」
閱讀本文大約需要 15 分鐘。
我經常聽到很多人討論,關於「把 Redis 當作佇列來用是否合適」的問題。
有些人表示贊成,他們認為 Redis 很輕量,用作佇列很方便。
也些人則反對,認為 Redis 會「丟」資料,最好還是用「專業」的佇列中介軟體更穩妥。
究竟哪種方案更好呢?
這篇文章,我就和你聊一聊把 Redis 當作佇列,究竟是否合適這個問題。
我會從簡單到複雜,一步步帶你梳理其中的細節,把這個問題真正的講清楚。
看完這篇文章後,我希望你對這個問題你會有全新的認識。
在文章的最後,我還會告訴你關於「技術選型」的思路,文章有點長,希望你可以耐心讀完。
從最簡單的開始:List 佇列
首先,我們先從最簡單的場景開始講起。
如果你的業務需求足夠簡單,想把 Redis 當作佇列來使用,肯定最先想到的就是使用 List 這個資料型別。
因為 List 底層的實現就是一個「連結串列」,在頭部和尾部操作元素,時間複雜度都是 O(1),這意味著它非常符合訊息佇列的模型。
如果把 List 當作佇列,你可以這麼來用。
生產者使用 LPUSH 釋出訊息:
127。0。0。1:6379> LPUSH queue msg1
(integer) 1
127。0。0。1:6379> LPUSH queue msg2
(integer) 2
消費者這一側,使用 RPOP 拉取訊息:
127。0。0。1:6379> RPOP queue
“msg1”
127。0。0。1:6379> RPOP queue
“msg2”
這個模型非常簡單,也很容易理解。
但這裡有個小問題,當佇列中已經沒有訊息了,消費者在執行 RPOP 時,會返回 NULL。
127。0。0。1:6379> RPOP queue
(nil) // 沒訊息了
而我們在編寫消費者邏輯時,一般是一個「死迴圈」,這個邏輯需要不斷地從佇列中拉取訊息進行處理,虛擬碼一般會這麼寫:
while true:
msg = redis。rpop(“queue”)
// 沒有訊息,繼續迴圈
if msg == null:
continue
// 處理訊息
handle(msg)
如果此時佇列為空,那消費者依舊會頻繁拉取訊息,這會造成「CPU 空轉」,不僅浪費 CPU 資源,還會對 Redis 造成壓力。
怎麼解決這個問題呢?
也很簡單,當佇列為空時,我們可以「休眠」一會,再去嘗試拉取訊息。程式碼可以修改成這樣:
while true:
msg = redis。rpop(“queue”)
// 沒有訊息,休眠2s
if msg == null:
sleep(2)
continue
// 處理訊息
handle(msg)
這就解決了 CPU 空轉問題。
這個問題雖然解決了,但又帶來另外一個問題:當消費者在休眠等待時,有新訊息來了,那消費者處理新訊息就會存在「延遲」。
假設設定的休眠時間是 2s,那新訊息最多存在 2s 的延遲。
要想縮短這個延遲,只能減小休眠的時間。但休眠時間越小,又有可能引發 CPU 空轉問題。
魚和熊掌不可兼得。
那如何做,既能及時處理新訊息,還能避免 CPU 空轉呢?
Redis 是否存在這樣一種機制:如果佇列為空,消費者在拉取訊息時就「阻塞等待」,一旦有新訊息過來,就通知我的消費者立即處理新訊息呢?
幸運的是,Redis 確實提供了「阻塞式」拉取訊息的命令:BRPOP / BLPOP,這裡的 B 指的是阻塞(Block)。
現在,你可以這樣來拉取訊息了:
while true:
// 沒訊息阻塞等待,0表示不設定超時時間
msg = redis。brpop(“queue”, 0)
if msg == null:
continue
// 處理訊息
handle(msg)
使用 BRPOP 這種阻塞式方式拉取訊息時,還支援傳入一個「超時時間」,如果設定為 0,則表示不設定超時,直到有新訊息才返回,否則會在指定的超時時間後返回 NULL。
這個方案不錯,既兼顧了效率,還避免了 CPU 空轉問題,一舉兩得。
注意:如果設定的超時時間太長,這個連線太久沒有活躍過,可能會被 Redis Server 判定為無效連線,之後 Redis Server 會強制把這個客戶端踢下線。所以,採用這種方案,客戶端要有重連機制。
解決了訊息處理不及時的問題,你可以再思考一下,這種佇列模型,有什麼缺點?
我們一起來分析一下:
不支援重複消費
:消費者拉取訊息後,這條訊息就從 List 中刪除了,無法被其它消費者再次消費,即不支援多個消費者消費同一批資料
訊息丟失
:消費者拉取到訊息後,如果發生異常宕機,那這條訊息就丟失了
第一個問題是功能上的,使用 List 做訊息佇列,它僅僅支援最簡單的,一組生產者對應一組消費者,不能滿足多組生產者和消費者的業務場景。
第二個問題就比較棘手了,因為從 List 中 POP 一條訊息出來後,這條訊息就會立即從連結串列中刪除了。也就是說,無論消費者是否處理成功,這條訊息都沒辦法再次消費了。
這也意味著,如果消費者在處理訊息時異常宕機,那這條訊息就相當於丟失了。
針對這 2 個問題怎麼解決呢?我們一個個來看。
釋出/訂閱模型:Pub/Sub
從名字就能看出來,這個模組是 Redis 專門是針對「釋出/訂閱」這種佇列模型設計的。
它正好可以解決前面提到的第一個問題:重複消費。
即多組生產者、消費者的場景,我們來看它是如何做的。
Redis 提供了 PUBLISH / SUBSCRIBE 命令,來完成釋出、訂閱的操作。
假設你想開啟 2 個消費者,同時消費同一批資料,就可以按照以下方式來實現。
首先,使用 SUBSCRIBE 命令,啟動 2 個消費者,並「訂閱」同一個佇列。
// 2個消費者 都訂閱一個佇列
127。0。0。1:6379> SUBSCRIBE queue
Reading messages。。。 (press Ctrl-C to quit)
1) “subscribe”
2) “queue”
3) (integer) 1
此時,2 個消費者都會被阻塞住,等待新訊息的到來。
之後,再啟動一個生產者,釋出一條訊息。
127。0。0。1:6379> PUBLISH queue msg1
(integer) 1
這時,2 個消費者就會解除阻塞,收到生產者發來的新訊息。
127。0。0。1:6379> SUBSCRIBE queue
// 收到新訊息
1) “message”
2) “queue”
3) “msg1”
看到了麼,使用 Pub/Sub 這種方案,既支援阻塞式拉取訊息,還很好地滿足了多組消費者,消費同一批資料的業務需求。
除此之外,Pub/Sub 還提供了「匹配訂閱」模式,允許消費者根據一定規則,訂閱「多個」自己感興趣的佇列。
// 訂閱符合規則的佇列
127。0。0。1:6379> PSUBSCRIBE queue。*
Reading messages。。。 (press Ctrl-C to quit)
1) “psubscribe”
2) “queue。*”
3) (integer) 1
這裡的消費者,訂閱了 queue。* 相關的佇列訊息。
之後,生產者分別向 queue。p1 和 queue。p2 釋出訊息。
127。0。0。1:6379> PUBLISH queue。p1 msg1
(integer) 1
127。0。0。1:6379> PUBLISH queue。p2 msg2
(integer) 1
這時再看消費者,它就可以接收到這 2 個生產者的訊息了。
127。0。0。1:6379> PSUBSCRIBE queue。*
Reading messages。。。 (press Ctrl-C to quit)
。。。
// 來自queue。p1的訊息
1) “pmessage”
2) “queue。*”
3) “queue。p1”
4) “msg1”
// 來自queue。p2的訊息
1) “pmessage”
2) “queue。*”
3) “queue。p2”
4) “msg2”
我們可以看到,Pub/Sub 最大的優勢就是,支援多組生產者、消費者處理訊息。
講完了它的優點,那它有什麼缺點呢?
其實,Pub/Sub 最大問題是:
丟資料
。
如果發生以下場景,就有可能導致資料丟失:
消費者下線
Redis 宕機
訊息堆積
究竟是怎麼回事?
這其實與 Pub/Sub 的實現方式有很大關係。
Pub/Sub 在實現時非常簡單,它沒有基於任何資料型別,也沒有做任何的資料儲存,它只是單純地為生產者、消費者建立「資料轉發通道」,把符合規則的資料,從一端轉發到另一端。
一個完整的釋出、訂閱訊息處理流程是這樣的:
消費者訂閱指定佇列,Redis 就會記錄一個對映關係:佇列->消費者
生產者向這個佇列釋出訊息,那 Redis 就從對映關係中找出對應的消費者,把訊息轉發給它
看到了麼,整個過程中,沒有任何的資料儲存,一切都是實時轉發的。
這種設計方案,就導致了上面提到的那些問題。
例如,如果一個消費者異常掛掉了,它再重新上線後,只能接收新的訊息,在下線期間生產者釋出的訊息,因為找不到消費者,都會被丟棄掉。
如果所有消費者都下線了,那生產者釋出的訊息,因為找不到任何一個消費者,也會全部「丟棄」。
所以,當你在使用 Pub/Sub 時,一定要注意:
消費者必須先訂閱佇列,生產者才能釋出訊息,否則訊息會丟失。
這也是前面講例子時,我們讓消費者先訂閱佇列,之後才讓生產者釋出訊息的原因。
另外,因為 Pub/Sub 沒有基於任何資料型別實現,所以它也不具備「資料持久化」的能力。
也就是說,Pub/Sub 的相關操作,不會寫入到 RDB 和 AOF 中,當 Redis 宕機重啟,Pub/Sub 的資料也會全部丟失。
最後,我們來看 Pub/Sub 在處理「訊息積壓」時,為什麼也會丟資料?
當消費者的速度,跟不上生產者時,就會導致資料積壓的情況發生。
如果採用 List 當作佇列,訊息積壓時,會導致這個連結串列很長,最直接的影響就是,Redis 記憶體會持續增長,直到消費者把所有資料都從連結串列中取出。
但 Pub/Sub 的處理方式卻不一樣,當訊息積壓時,有可能會導致
消費失敗和訊息丟失
!
這是怎麼回事?
還是回到 Pub/Sub 的實現細節上來說。
每個消費者訂閱一個佇列時,Redis 都會在 Server 上給這個消費者在分配一個「緩衝區」,這個緩衝區其實就是一塊記憶體。
當生產者釋出訊息時,Redis 先把訊息寫到對應消費者的緩衝區中。
之後,消費者不斷地從緩衝區讀取訊息,處理訊息。
但是,問題就出在這個緩衝區上。
因為這個緩衝區其實是有「上限」的(可配置),如果消費者拉取訊息很慢,就會造成生產者釋出到緩衝區的訊息開始積壓,緩衝區記憶體持續增長。
如果超過了緩衝區配置的上限,此時,Redis 就會「強制」把這個消費者踢下線。
這時消費者就會消費失敗,也會丟失資料。
如果你有看過 Redis 的配置檔案,可以看到這個緩衝區的預設配置:client-output-buffer-limit pubsub 32mb 8mb 60。
它的引數含義如下:
32mb:緩衝區一旦超過 32MB,Redis 直接強制把消費者踢下線
8mb + 60:緩衝區超過 8MB,並且持續 60 秒,Redis 也會把消費者踢下線
Pub/Sub 的這一點特點,是與 List 作佇列差異比較大的。
從這裡你應該可以看出,
List 其實是屬於「拉」模型,而 Pub/Sub 其實屬於「推」模型
。
List 中的資料可以一直積壓在記憶體中,消費者什麼時候來「拉」都可以。
但 Pub/Sub 是把訊息先「推」到消費者在 Redis Server 上的緩衝區中,然後等消費者再來取。
當生產、消費速度不匹配時,就會導致緩衝區的記憶體開始膨脹,Redis 為了控制緩衝區的上限,所以就有了上面講到的,強制把消費者踢下線的機制。
好了,現在我們總結一下 Pub/Sub 的優缺點:
支援釋出 / 訂閱,支援多組生產者、消費者處理訊息
消費者下線,資料會丟失
不支援資料持久化,Redis 宕機,資料也會丟失
訊息堆積,緩衝區溢位,消費者會被強制踢下線,資料也會丟失
有沒有發現,除了第一個是優點之外,剩下的都是缺點。
所以,很多人看到 Pub/Sub 的特點後,覺得這個功能很「雞肋」。
也正是以上原因,Pub/Sub 在實際的應用場景中用得並不多。
目前只有哨兵叢集和 Redis 例項通訊時,採用了 Pub/Sub 的方案,因為哨兵正好符合即時通訊的業務場景。
我們再來看一下,Pub/Sub 有沒有解決,訊息處理時異常宕機,無法再次消費的問題呢?
其實也不行,Pub/Sub 從緩衝區取走資料之後,資料就從 Redis 緩衝區刪除了,消費者發生異常,自然也無法再次重新消費。
好,現在我們重新梳理一下,我們在使用訊息佇列時的需求。
當我們在使用一個訊息佇列時,希望它的功能如下:
支援阻塞等待拉取訊息
支援釋出 / 訂閱模式
消費失敗,可重新消費,訊息不丟失
例項宕機,訊息不丟失,資料可持久化
訊息可堆積
Redis 除了 List 和 Pub/Sub 之外,還有符合這些要求的資料型別嗎?
其實,Redis 的作者也看到了以上這些問題,也一直在朝著這些方向努力著。
Redis 作者在開發 Redis 期間,還另外開發了一個開源專案 disque。
這個專案的定位,就是一個基於記憶體的分散式訊息佇列中介軟體。
但由於種種原因,這個專案一直不溫不火。
終於,在 Redis 5。0 版本,作者把 disque 功能移植到了 Redis 中,並給它定義了一個新的資料型別:
Stream
。
下面我們就來看看,它能符合上面提到的這些要求嗎?
趨於成熟的佇列:Stream
我們來看 Stream 是如何解決上面這些問題的。
我們依舊從簡單到複雜,依次來看 Stream 在做訊息佇列時,是如何處理的?
首先,Stream 透過 XADD 和 XREAD 完成最簡單的生產、消費模型:
XADD:釋出訊息
XREAD:讀取訊息
生產者釋出 2 條訊息:
// *表示讓Redis自動生成訊息ID
127。0。0。1:6379> XADD queue * name zhangsan
“1618469123380-0”
127。0。0。1:6379> XADD queue * name lisi
“1618469127777-0”
使用 XADD 命令釋出訊息,其中的「*」表示讓 Redis 自動生成唯一的訊息 ID。
這個訊息 ID 的格式是「時間戳-自增序號」。
消費者拉取訊息:
// 從開頭讀取5條訊息,0-0表示從開頭讀取
127。0。0。1:6379> XREAD COUNT 5 STREAMS queue 0-0
1) 1) “queue”
2) 1) 1) “1618469123380-0”
2) 1) “name”
2) “zhangsan”
2) 1) “1618469127777-0”
2) 1) “name”
2) “lisi”
如果想繼續拉取訊息,需要傳入上一條訊息的 ID:
127。0。0。1:6379> XREAD COUNT 5 STREAMS queue 1618469127777-0
(nil)
沒有訊息,Redis 會返回 NULL。
以上就是 Stream 最簡單的生產、消費。
這裡不再重點介紹 Stream 命令的各種引數,我在例子中演示時,凡是大寫的單詞都是「固定」引數,凡是小寫的單詞,都是可以自己定義的,例如佇列名、訊息長度等等,下面的例子規則也是一樣,為了方便你理解,這裡有必要提醒一下。
下面我們來看,針對前面提到的訊息佇列要求,Stream 都是如何解決的?
1) Stream 是否支援「阻塞式」拉取訊息?
可以的,在讀取訊息時,只需要增加 BLOCK 引數即可。
// BLOCK 0 表示阻塞等待,不設定超時時間
127。0。0。1:6379> XREAD COUNT 5 BLOCK 0 STREAMS queue 1618469127777-0
這時,消費者就會阻塞等待,直到生產者釋出新的訊息才會返回。
2) Stream 是否支援釋出 / 訂閱模式?
也沒問題,Stream 透過以下命令完成釋出訂閱:
XGROUP:建立消費者組
XREADGROUP:在指定消費組下,開啟消費者拉取訊息
下面我們來看具體如何做?
首先,生產者依舊釋出 2 條訊息:
127。0。0。1:6379> XADD queue * name zhangsan
“1618470740565-0”
127。0。0。1:6379> XADD queue * name lisi
“1618470743793-0”
之後,我們想要開啟 2 組消費者處理同一批資料,就需要建立 2 個消費者組:
// 建立消費者組1,0-0表示從頭拉取訊息
127。0。0。1:6379> XGROUP CREATE queue group1 0-0
OK
// 建立消費者組2,0-0表示從頭拉取訊息
127。0。0。1:6379> XGROUP CREATE queue group2 0-0
OK
消費者組建立好之後,我們可以給每個「消費者組」下面掛一個「消費者」,讓它們分別處理同一批資料。
第一個消費組開始消費:
// group1的consumer開始消費,>表示拉取最新資料
127。0。0。1:6379> XREADGROUP GROUP group1 consumer COUNT 5 STREAMS queue >
1) 1) “queue”
2) 1) 1) “1618470740565-0”
2) 1) “name”
2) “zhangsan”
2) 1) “1618470743793-0”
2) 1) “name”
2) “lisi”
同樣地,第二個消費組開始消費:
// group2的consumer開始消費,>表示拉取最新資料
127。0。0。1:6379> XREADGROUP GROUP group2 consumer COUNT 5 STREAMS queue >
1) 1) “queue”
2) 1) 1) “1618470740565-0”
2) 1) “name”
2) “zhangsan”
2) 1) “1618470743793-0”
2) 1) “name”
2) “lisi”
我們可以看到,這 2 組消費者,都可以獲取同一批資料進行處理了。
這樣一來,就達到了多組消費者「訂閱」消費的目的。
3) 訊息處理時異常,Stream 能否保證訊息不丟失,重新消費?
除了上面拉取訊息時用到了訊息 ID,這裡為了保證重新消費,也要用到這個訊息 ID。
當一組消費者處理完訊息後,需要執行 XACK 命令告知 Redis,這時 Redis 就會把這條訊息標記為「處理完成」。
// group1下的 1618472043089-0 訊息已處理完成
127。0。0。1:6379> XACK queue group1 1618472043089-0
如果消費者異常宕機,肯定不會發送 XACK,那麼 Redis 就會依舊保留這條訊息。
待這組消費者重新上線後,Redis 就會把之前沒有處理成功的資料,重新發給這個消費者。這樣一來,即使消費者異常,也不會丟失資料了。
// 消費者重新上線,0-0表示重新拉取未ACK的訊息
127。0。0。1:6379> XREADGROUP GROUP group1 consumer1 COUNT 5 STREAMS queue 0-0
// 之前沒消費成功的資料,依舊可以重新消費
1) 1) “queue”
2) 1) 1) “1618472043089-0”
2) 1) “name”
2) “zhangsan”
2) 1) “1618472045158-0”
2) 1) “name”
2) “lisi”
4) Stream 資料會寫入到 RDB 和 AOF 做持久化嗎?
Stream 是新增加的資料型別,它與其它資料型別一樣,每個寫操作,也都會寫入到 RDB 和 AOF 中。
我們只需要配置好持久化策略,這樣的話,就算 Redis 宕機重啟,Stream 中的資料也可以從 RDB 或 AOF 中恢復回來。
5) 訊息堆積時,Stream 是怎麼處理的?
其實,當訊息佇列發生訊息堆積時,一般只有 2 個解決方案:
生產者限流:避免消費者處理不及時,導致持續積壓
丟棄訊息:中介軟體丟棄舊訊息,只保留固定長度的新訊息
而 Redis 在實現 Stream 時,採用了第 2 個方案。
在釋出訊息時,你可以指定佇列的最大長度,防止佇列積壓導致記憶體爆炸。
// 佇列長度最大10000
127。0。0。1:6379> XADD queue MAXLEN 10000 * name zhangsan
“1618473015018-0”
當佇列長度超過上限後,舊訊息會被刪除,只保留固定長度的新訊息。
這麼來看,Stream 在訊息積壓時,如果指定了最大長度,還是有可能丟失訊息的。
除了以上介紹到的命令,Stream 還支援檢視訊息長度(XLEN)、檢視消費者狀態(XINFO)等命令,使用也比較簡單,你可以查詢官方文件瞭解一下,這裡就不過多介紹了。
好了,透過以上介紹,我們可以看到,Redis 的 Stream 幾乎覆蓋到了訊息佇列的各種場景,是不是覺得很完美?
既然它的功能這麼強大,這是不是意味著,Redis 真的可以作為專業的訊息佇列中介軟體來使用呢?
但是還「差一點」,就算 Redis 能做到以上這些,也只是「趨近於」專業的訊息佇列。
原因在於 Redis 本身的一些問題,如果把其定位成訊息佇列,還是有些欠缺的。
到這裡,就不得不把 Redis 與專業的佇列中介軟體做對比了。
下面我們就來看一下,Redis 在作佇列時,到底還有哪些欠缺?
與專業的訊息佇列對比
其實,一個專業的訊息佇列,必須要做到兩大塊:
訊息不丟
訊息可堆積
前面我們討論的重點,很大篇幅圍繞的是第一點展開的。
這裡我們換個角度,從一個訊息佇列的「使用模型」來分析一下,怎麼做,才能保證資料不丟?
使用一個訊息佇列,其實就分為三大塊:
生產者、佇列中介軟體、消費者
。
訊息是否會發生丟失,其重點也就在於以下 3 個環節:
生產者會不會丟訊息?
消費者會不會丟訊息?
佇列中介軟體會不會丟訊息?
1) 生產者會不會丟訊息?
當生產者在釋出訊息時,可能發生以下異常情況:
訊息沒發出去:網路故障或其它問題導致釋出失敗,中介軟體直接返回失敗
不確定是否釋出成功:網路問題導致釋出超時,可能資料已傳送成功,但讀取響應結果超時了
如果是情況 1,訊息根本沒發出去,那麼重新發一次就好了。
如果是情況 2,生產者沒辦法知道訊息到底有沒有發成功?所以,為了避免訊息丟失,它也只能繼續重試,直到釋出成功為止。
生產者一般會設定一個最大重試次數,超過上限依舊失敗,需要記錄日誌報警處理。
也就是說,生產者為了避免訊息丟失,只能採用失敗重試的方式來處理。
但發現沒有?這也意味著訊息可能會重複傳送。
是的,在使用訊息佇列時,要保證訊息不丟,寧可重發,也不能丟棄。
那消費者這邊,就需要多做一些邏輯了。
對於敏感業務,當消費者收到重複資料資料時,要設計冪等邏輯,保證業務的正確性。
從這個角度來看,生產者會不會丟訊息,取決於生產者對於異常情況的處理是否合理。
所以,無論是 Redis 還是專業的佇列中介軟體,生產者在這一點上都是可以保證訊息不丟的。
2) 消費者會不會丟訊息?
這種情況就是我們前面提到的,消費者拿到訊息後,還沒處理完成,就異常宕機了,那消費者還能否重新消費失敗的訊息?
要解決這個問題,消費者在處理完訊息後,必須「告知」佇列中介軟體,佇列中介軟體才會把標記已處理,否則仍舊把這些資料發給消費者。
這種方案需要消費者和中介軟體互相配合,才能保證消費者這一側的訊息不丟。
無論是 Redis 的 Stream,還是專業的佇列中介軟體,例如 RabbitMQ、Kafka,其實都是這麼做的。
所以,從這個角度來看,Redis 也是合格的。
3) 佇列中介軟體會不會丟訊息?
前面 2 個問題都比較好處理,只要客戶端和服務端配合好,就能保證生產端、消費端都不丟訊息。
但是,如果佇列中介軟體本身就不可靠呢?
畢竟生產者和消費這都依賴它,如果它不可靠,那麼生產者和消費者無論怎麼做,都無法保證資料不丟。
在這個方面,Redis 其實沒有達到要求。
Redis 在以下 2 個場景下,都會導致資料丟失。
AOF 持久化配置為每秒寫盤,但這個寫盤過程是非同步的,Redis 宕機時會存在資料丟失的可能
主從複製也是非同步的,主從切換時,也存在丟失資料的可能(從庫還未同步完成主庫發來的資料,就被提成主庫)
基於以上原因我們可以看到,
Redis 本身的無法保證嚴格的資料完整性
。
所以,如果把 Redis 當做訊息佇列,在這方面是有可能導致資料丟失的。
再來看那些專業的訊息佇列中介軟體是如何解決這個問題的?
像 RabbitMQ 或 Kafka 這類專業的佇列中介軟體,在使用時,一般是部署一個叢集,生產者在釋出訊息時,佇列中介軟體通常會寫「多個節點」,以此保證訊息的完整性。這樣一來,即便其中一個節點掛了,也能保證叢集的資料不丟失。
也正因為如此,RabbitMQ、Kafka在設計時也更復雜。畢竟,它們是專門針對佇列場景設計的。
但 Redis 的定位則不同,它的定位更多是當作快取來用,它們兩者在這個方面肯定是存在差異的。
最後,我們來看訊息積壓怎麼辦?
4) 訊息積壓怎麼辦?
因為 Redis 的資料都儲存在記憶體中,這就意味著一旦發生訊息積壓,則會導致 Redis 的記憶體持續增長,如果超過機器記憶體上限,就會面臨被 OOM 的風險。
所以,Redis 的 Stream 提供了可以指定佇列最大長度的功能,就是為了避免這種情況發生。
但 Kafka、RabbitMQ 這類訊息佇列就不一樣了,它們的資料都會儲存在磁碟上,磁碟的成本要比記憶體小得多,當訊息積壓時,無非就是多佔用一些磁碟空間,相比於記憶體,在面對積壓時也會更加「坦然」。
綜上,我們可以看到,把 Redis 當作佇列來使用時,始終面臨的 2 個問題:
Redis 本身可能會丟資料
面對訊息積壓,Redis 記憶體資源緊張
到這裡,Redis 是否可以用作佇列,我想這個答案你應該會比較清晰了。
如果你的業務場景足夠簡單,對於資料丟失不敏感,而且訊息積壓機率比較小的情況下,把 Redis 當作佇列是完全可以的。
而且,Redis 相比於 Kafka、RabbitMQ,部署和運維也更加輕量。
如果你的業務場景對於資料丟失非常敏感,而且寫入量非常大,訊息積壓時會佔用很多的機器資源,那麼我建議你使用專業的訊息佇列中介軟體。
總結
好了,總結一下。這篇文章我們從「Redis 能否用作佇列」這個角度出發,介紹了 List、Pub/Sub、Stream 在做佇列的使用方式,以及它們各自的優劣。
之後又把 Redis 和專業的訊息佇列中介軟體做對比,發現 Redis 的不足之處。
最後,我們得出 Redis 做佇列的合適場景。
這裡我也列了一個表格,總結了它們各自的優缺點。
後記
最後,我想和你再聊一聊關於「
技術方案選型
」的問題。
你應該也看到了,這篇文章雖然始於 Redis,但並不止於 Redis。
我們在分析 Redis 細節時,一直在提出問題,然後尋找更好的解決方案,在文章最後,又聊到一個專業的訊息佇列應該怎麼做。
其實,我們在討論技術選型時,就是一個關於如何取捨的問題。
而這裡我想傳達給你的資訊是,
在面對技術選型時,不要不經過思考就覺得哪個方案好,哪個方案不好
。
你需要根據具體場景具體分析,這裡我把這個分析過程分為 2 個層面:
業務功能角度
技術資源角度
這篇文章所講到的內容,都是以業務功能角度出發做決策的。
但這裡的第二點,從技術資源角度出發,其實也很重要。
技術資源的角度是說,
你所處的公司環境、技術資源能否匹配這些技術方案
。
這個怎麼解釋呢?
簡單來講,就是你所在的公司、團隊,是否有匹配的資源能 hold 住這些技術方案。
我們都知道 Kafka、RabbitMQ 是非常專業的訊息中介軟體,但它們的部署和運維,相比於 Redis 來說,也會更復雜一些。
如果你在一個大公司,公司本身就有優秀的運維團隊,那麼使用這些中介軟體肯定沒問題,因為有足夠優秀的人能 hold 住這些中介軟體,公司也會投入人力和時間在這個方向上。
但如果你是在一個初創公司,業務正處在快速發展期,暫時沒有能 hold 住這些中介軟體的團隊和人,如果貿然使用這些元件,當發生故障時,排查問題也會變得很困難,甚至會阻礙業務的發展。
而這種情形下,如果公司的技術人員對於 Redis 都很熟,綜合評估來看,Redis 也基本可以滿足業務 90% 的需求,那當下選擇 Redis 未必不是一個好的決策。
所以,
做技術選型不只是技術問題,還與人、團隊、管理、組織結構有關
。
也正是因為這些原因,當你在和別人討論技術選型問題時,你會發現每個公司的做法都不相同。
畢竟每個公司所處的環境和文化不一樣,做出的決策當然就會各有差異。
如果你不瞭解這其中的邏輯,那在做技術選型時,只會趨於表面現象,無法深入到問題根源。
而一旦你理解了這個邏輯,那麼你在看待這個問題時,不僅對於技術會有更加深刻認識,對技術資源和人的把握,也會更加清晰。
希望你以後在做技術選型時,能夠把這些因素也考慮在內,這對你的技術成長之路也是非常有幫助的。
臥槽!清華還是牛逼:2021 元宇宙研究報告!
HTTP 2。0 ,有點炸 !