日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

Redis 究竟适不适合当队列来用?

發(fā)布時間:2024/8/23 数据库 48 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Redis 究竟适不适合当队列来用? 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

?

作者 |?Magic Kaito

來源 | 水滴與銀彈

我經(jīng)常聽到很多人討論,關(guān)于「把 Redis 當作隊列來用是否合適」的問題。

有些人表示贊成,他們認為 Redis 很輕量,用作隊列很方便。

也些人則反對,認為 Redis 會「丟」數(shù)據(jù),最好還是用「專業(yè)」的隊列中間件更穩(wěn)妥。

究竟哪種方案更好呢?

這篇文章,我就和你聊一聊把 Redis 當作隊列,究竟是否合適這個問題。

我會從簡單到復雜,一步步帶你梳理其中的細節(jié),把這個問題真正的講清楚。

看完這篇文章后,我希望你對這個問題你會有全新的認識。

從最簡單的開始:List 隊列

首先,我們先從最簡單的場景開始講起。

如果你的業(yè)務需求足夠簡單,想把 Redis 當作隊列來使用,肯定最先想到的就是使用 List 這個數(shù)據(jù)類型。

因為 List 底層的實現(xiàn)就是一個「鏈表」,在頭部和尾部操作元素,時間復雜度都是 O(1),這意味著它非常符合消息隊列的模型。

如果把 List 當作隊列,你可以這么來用。

生產(chǎn)者使用 LPUSH 發(fā)布消息:

127.0.0.1:6379>?LPUSH?queue?msg1 (integer)?1 127.0.0.1:6379>?LPUSH?queue?msg2 (integer)?2

消費者這一側(cè),使用 RPOP 拉取消息:

127.0.0.1:6379>?RPOP?queue "msg1" 127.0.0.1:6379>?RPOP?queue "msg2"

這個模型非常簡單,也很容易理解。

但這里有個小問題,當隊列中已經(jīng)沒有消息了,消費者在執(zhí)行 RPOP 時,會返回 NULL。

127.0.0.1:6379>?RPOP?queue (nil)???//?沒消息了

而我們在編寫消費者邏輯時,一般是一個「死循環(huán)」,這個邏輯需要不斷地從隊列中拉取消息進行處理,偽代碼一般會這么寫:

while?true:msg?=?redis.rpop("queue")//?沒有消息,繼續(xù)循環(huán)if?msg?==?null:continue//?處理消息handle(msg)

如果此時隊列為空,那消費者依舊會頻繁拉取消息,這會造成「CPU 空轉(zhuǎn)」,不僅浪費 CPU 資源,還會對 Redis 造成壓力。

怎么解決這個問題呢?

也很簡單,當隊列為空時,我們可以「休眠」一會,再去嘗試拉取消息。代碼可以修改成這樣:

while?true:msg?=?redis.rpop("queue")//?沒有消息,休眠2sif?msg?==?null:sleep(2)continue//?處理消息????????handle(msg)

這就解決了 CPU 空轉(zhuǎn)問題。

這個問題雖然解決了,但又帶來另外一個問題:當消費者在休眠等待時,有新消息來了,那消費者處理新消息就會存在「延遲」。

假設(shè)設(shè)置的休眠時間是 2s,那新消息最多存在 2s 的延遲。

要想縮短這個延遲,只能減小休眠的時間。但休眠時間越小,又有可能引發(fā) CPU 空轉(zhuǎn)問題。

魚和熊掌不可兼得。

那如何做,既能及時處理新消息,還能避免 CPU 空轉(zhuǎn)呢?

Redis 是否存在這樣一種機制:如果隊列為空,消費者在拉取消息時就「阻塞等待」,一旦有新消息過來,就通知我的消費者立即處理新消息呢?

幸運的是,Redis 確實提供了「阻塞式」拉取消息的命令:BRPOP / BLPOP,這里的 B 指的是阻塞(Block)。

現(xiàn)在,你可以這樣來拉取消息了:

while?true://?沒消息阻塞等待,0表示不設(shè)置超時時間msg?=?redis.brpop("queue",?0)if?msg?==?null:continue//?處理消息handle(msg)

使用 BRPOP 這種阻塞式方式拉取消息時,還支持傳入一個「超時時間」,如果設(shè)置為 0,則表示不設(shè)置超時,直到有新消息才返回,否則會在指定的超時時間后返回 NULL。

這個方案不錯,既兼顧了效率,還避免了 CPU 空轉(zhuǎn)問題,一舉兩得。

注意:如果設(shè)置的超時時間太長,這個連接太久沒有活躍過,可能會被 Redis Server 判定為無效連接,之后 Redis Server 會強制把這個客戶端踢下線。所以,采用這種方案,客戶端要有重連機制。

解決了消息處理不及時的問題,你可以再思考一下,這種隊列模型,有什么缺點?

我們一起來分析一下:

  • 不支持重復消費:消費者拉取消息后,這條消息就從 List 中刪除了,無法被其它消費者再次消費,即不支持多個消費者消費同一批數(shù)據(jù)

  • 消息丟失:消費者拉取到消息后,如果發(fā)生異常宕機,那這條消息就丟失了

  • 第一個問題是功能上的,使用 List 做消息隊列,它僅僅支持最簡單的,一組生產(chǎn)者對應一組消費者,不能滿足多組生產(chǎn)者和消費者的業(yè)務場景。

    第二個問題就比較棘手了,因為從 List 中 POP 一條消息出來后,這條消息就會立即從鏈表中刪除了。也就是說,無論消費者是否處理成功,這條消息都沒辦法再次消費了。

    這也意味著,如果消費者在處理消息時異常宕機,那這條消息就相當于丟失了。

    針對這 2 個問題怎么解決呢?我們一個個來看。

    發(fā)布/訂閱模型:Pub/Sub

    從名字就能看出來,這個模塊是 Redis 專門是針對「發(fā)布/訂閱」這種隊列模型設(shè)計的。

    它正好可以解決前面提到的第一個問題:重復消費。

    即多組生產(chǎn)者、消費者的場景,我們來看它是如何做的。

    Redis 提供了 PUBLISH / SUBSCRIBE 命令,來完成發(fā)布、訂閱的操作。

    假設(shè)你想開啟 2 個消費者,同時消費同一批數(shù)據(jù),就可以按照以下方式來實現(xiàn)。

    首先,使用 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 個消費者都會被阻塞住,等待新消息的到來。

    之后,再啟動一個生產(chǎn)者,發(fā)布一條消息。

    127.0.0.1:6379>?PUBLISH?queue?msg1 (integer)?1

    這時,2 個消費者就會解除阻塞,收到生產(chǎn)者發(fā)來的新消息。

    127.0.0.1:6379>?SUBSCRIBE?queue //?收到新消息 1)?"message" 2)?"queue" 3)?"msg1"

    看到了么,使用 Pub/Sub 這種方案,既支持阻塞式拉取消息,還很好地滿足了多組消費者,消費同一批數(shù)據(jù)的業(yè)務需求。

    除此之外,Pub/Sub 還提供了「匹配訂閱」模式,允許消費者根據(jù)一定規(guī)則,訂閱「多個」自己感興趣的隊列。

    //?訂閱符合規(guī)則的隊列 127.0.0.1:6379>?PSUBSCRIBE?queue.* Reading?messages...?(press?Ctrl-C?to?quit) 1)?"psubscribe" 2)?"queue.*" 3)?(integer)?1

    這里的消費者,訂閱了 queue.* 相關(guān)的隊列消息。

    之后,生產(chǎn)者分別向 queue.p1 和 queue.p2 發(fā)布消息。

    127.0.0.1:6379>?PUBLISH?queue.p1?msg1 (integer)?1 127.0.0.1:6379>?PUBLISH?queue.p2?msg2 (integer)?1

    這時再看消費者,它就可以接收到這 2 個生產(chǎn)者的消息了。

    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 最大的優(yōu)勢就是,支持多組生產(chǎn)者、消費者處理消息。

    講完了它的優(yōu)點,那它有什么缺點呢?

    其實,Pub/Sub 最大問題是:丟數(shù)據(jù)

    如果發(fā)生以下場景,就有可能導致數(shù)據(jù)丟失:

  • 消費者下線

  • Redis 宕機

  • 消息堆積

  • 究竟是怎么回事?

    這其實與 Pub/Sub 的實現(xiàn)方式有很大關(guān)系。

    Pub/Sub 在實現(xiàn)時非常簡單,它沒有基于任何數(shù)據(jù)類型,也沒有做任何的數(shù)據(jù)存儲,它只是單純地為生產(chǎn)者、消費者建立「數(shù)據(jù)轉(zhuǎn)發(fā)通道」,把符合規(guī)則的數(shù)據(jù),從一端轉(zhuǎn)發(fā)到另一端。

    一個完整的發(fā)布、訂閱消息處理流程是這樣的:

  • 消費者訂閱指定隊列,Redis 就會記錄一個映射關(guān)系:隊列->消費者

  • 生產(chǎn)者向這個隊列發(fā)布消息,那 Redis 就從映射關(guān)系中找出對應的消費者,把消息轉(zhuǎn)發(fā)給它

  • 看到了么,整個過程中,沒有任何的數(shù)據(jù)存儲,一切都是實時轉(zhuǎn)發(fā)的。

    這種設(shè)計方案,就導致了上面提到的那些問題。

    例如,如果一個消費者異常掛掉了,它再重新上線后,只能接收新的消息,在下線期間生產(chǎn)者發(fā)布的消息,因為找不到消費者,都會被丟棄掉。

    如果所有消費者都下線了,那生產(chǎn)者發(fā)布的消息,因為找不到任何一個消費者,也會全部「丟棄」。

    所以,當你在使用 Pub/Sub 時,一定要注意:消費者必須先訂閱隊列,生產(chǎn)者才能發(fā)布消息,否則消息會丟失。

    這也是前面講例子時,我們讓消費者先訂閱隊列,之后才讓生產(chǎn)者發(fā)布消息的原因。

    另外,因為 Pub/Sub 沒有基于任何數(shù)據(jù)類型實現(xiàn),所以它也不具備「數(shù)據(jù)持久化」的能力。

    也就是說,Pub/Sub 的相關(guān)操作,不會寫入到 RDB 和 AOF 中,當 Redis 宕機重啟,Pub/Sub 的數(shù)據(jù)也會全部丟失。

    最后,我們來看 Pub/Sub 在處理「消息積壓」時,為什么也會丟數(shù)據(jù)?

    當消費者的速度,跟不上生產(chǎn)者時,就會導致數(shù)據(jù)積壓的情況發(fā)生。

    如果采用 List 當作隊列,消息積壓時,會導致這個鏈表很長,最直接的影響就是,Redis 內(nèi)存會持續(xù)增長,直到消費者把所有數(shù)據(jù)都從鏈表中取出。

    但 Pub/Sub 的處理方式卻不一樣,當消息積壓時,有可能會導致消費失敗和消息丟失

    這是怎么回事?

    還是回到 Pub/Sub 的實現(xiàn)細節(jié)上來說。

    每個消費者訂閱一個隊列時,Redis 都會在 Server 上給這個消費者在分配一個「緩沖區(qū)」,這個緩沖區(qū)其實就是一塊內(nèi)存。

    當生產(chǎn)者發(fā)布消息時,Redis 先把消息寫到對應消費者的緩沖區(qū)中。

    之后,消費者不斷地從緩沖區(qū)讀取消息,處理消息。

    但是,問題就出在這個緩沖區(qū)上。

    因為這個緩沖區(qū)其實是有「上限」的(可配置),如果消費者拉取消息很慢,就會造成生產(chǎn)者發(fā)布到緩沖區(qū)的消息開始積壓,緩沖區(qū)內(nèi)存持續(xù)增長。

    如果超過了緩沖區(qū)配置的上限,此時,Redis 就會「強制」把這個消費者踢下線。

    這時消費者就會消費失敗,也會丟失數(shù)據(jù)。

    如果你有看過 Redis 的配置文件,可以看到這個緩沖區(qū)的默認配置:client-output-buffer-limit pubsub 32mb 8mb 60。

    它的參數(shù)含義如下:

    • 32mb:緩沖區(qū)一旦超過 32MB,Redis 直接強制把消費者踢下線

    • 8mb + 60:緩沖區(qū)超過 8MB,并且持續(xù) 60 秒,Redis 也會把消費者踢下線

    Pub/Sub 的這一點特點,是與 List 作隊列差異比較大的。

    從這里你應該可以看出,List 其實是屬于「拉」模型,而 Pub/Sub 其實屬于「推」模型

    List 中的數(shù)據(jù)可以一直積壓在內(nèi)存中,消費者什么時候來「拉」都可以。

    但 Pub/Sub 是把消息先「推」到消費者在 Redis Server 上的緩沖區(qū)中,然后等消費者再來取。

    當生產(chǎn)、消費速度不匹配時,就會導致緩沖區(qū)的內(nèi)存開始膨脹,Redis 為了控制緩沖區(qū)的上限,所以就有了上面講到的,強制把消費者踢下線的機制。

    好了,現(xiàn)在我們總結(jié)一下 Pub/Sub 的優(yōu)缺點:

  • 支持發(fā)布 / 訂閱,支持多組生產(chǎn)者、消費者處理消息

  • 消費者下線,數(shù)據(jù)會丟失

  • 不支持數(shù)據(jù)持久化,Redis 宕機,數(shù)據(jù)也會丟失

  • 消息堆積,緩沖區(qū)溢出,消費者會被強制踢下線,數(shù)據(jù)也會丟失

  • 有沒有發(fā)現(xiàn),除了第一個是優(yōu)點之外,剩下的都是缺點。

    所以,很多人看到 Pub/Sub 的特點后,覺得這個功能很「雞肋」。

    也正是以上原因,Pub/Sub 在實際的應用場景中用得并不多。

    目前只有哨兵集群和 Redis 實例通信時,采用了 Pub/Sub 的方案,因為哨兵正好符合即時通訊的業(yè)務場景。

    我們再來看一下,Pub/Sub 有沒有解決,消息處理時異常宕機,無法再次消費的問題呢?

    其實也不行,Pub/Sub 從緩沖區(qū)取走數(shù)據(jù)之后,數(shù)據(jù)就從 Redis 緩沖區(qū)刪除了,消費者發(fā)生異常,自然也無法再次重新消費。

    好,現(xiàn)在我們重新梳理一下,我們在使用消息隊列時的需求。

    當我們在使用一個消息隊列時,希望它的功能如下:

    • 支持阻塞等待拉取消息

    • 支持發(fā)布 / 訂閱模式

    • 消費失敗,可重新消費,消息不丟失

    • 實例宕機,消息不丟失,數(shù)據(jù)可持久化

    • 消息可堆積

    Redis 除了 List 和 Pub/Sub 之外,還有符合這些要求的數(shù)據(jù)類型嗎?

    其實,Redis 的作者也看到了以上這些問題,也一直在朝著這些方向努力著。

    Redis 作者在開發(fā) Redis 期間,還另外開發(fā)了一個開源項目 disque。

    這個項目的定位,就是一個基于內(nèi)存的分布式消息隊列中間件。

    但由于種種原因,這個項目一直不溫不火。

    終于,在 Redis 5.0 版本,作者把 disque 功能移植到了 Redis 中,并給它定義了一個新的數(shù)據(jù)類型:Stream

    下面我們就來看看,它能符合上面提到的這些要求嗎?

    趨于成熟的隊列:Stream

    我們來看 Stream 是如何解決上面這些問題的。

    我們依舊從簡單到復雜,依次來看 Stream 在做消息隊列時,是如何處理的?

    首先,Stream 通過 XADD 和 XREAD 完成最簡單的生產(chǎn)、消費模型:

    • XADD:發(fā)布消息

    • XREAD:讀取消息

    生產(chǎn)者發(fā)布 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 命令發(fā)布消息,其中的「*」表示讓 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"

    如果想繼續(xù)拉取消息,需要傳入上一條消息的 ID:

    127.0.0.1:6379>?XREAD?COUNT?5?STREAMS?queue?1618469127777-0 (nil)

    沒有消息,Redis 會返回 NULL。

    以上就是 Stream 最簡單的生產(chǎn)、消費。

    這里不再重點介紹 Stream 命令的各種參數(shù),我在例子中演示時,凡是大寫的單詞都是「固定」參數(shù),凡是小寫的單詞,都是可以自己定義的,例如隊列名、消息長度等等,下面的例子規(guī)則也是一樣,為了方便你理解,這里有必要提醒一下。

    下面我們來看,針對前面提到的消息隊列要求,Stream 都是如何解決的?

    1) Stream 是否支持「阻塞式」拉取消息?

    可以的,在讀取消息時,只需要增加 BLOCK 參數(shù)即可。

    //?BLOCK?0?表示阻塞等待,不設(shè)置超時時間 127.0.0.1:6379>?XREAD?COUNT?5?BLOCK?0?STREAMS?queue?1618469127777-0

    這時,消費者就會阻塞等待,直到生產(chǎn)者發(fā)布新的消息才會返回。

    2) Stream 是否支持發(fā)布 / 訂閱模式?

    也沒問題,Stream 通過以下命令完成發(fā)布訂閱:

    • XGROUP:創(chuàng)建消費者組

    • XREADGROUP:在指定消費組下,開啟消費者拉取消息

    下面我們來看具體如何做?

    首先,生產(chǎn)者依舊發(fā)布 2 條消息:

    127.0.0.1:6379>?XADD?queue?*?name?zhangsan "1618470740565-0" 127.0.0.1:6379>?XADD?queue?*?name?lisi "1618470743793-0"

    之后,我們想要開啟 2 組消費者處理同一批數(shù)據(jù),就需要創(chuàng)建 2 個消費者組:

    //?創(chuàng)建消費者組1,0-0表示從頭拉取消息 127.0.0.1:6379>?XGROUP?CREATE?queue?group1?0-0 OK //?創(chuàng)建消費者組2,0-0表示從頭拉取消息 127.0.0.1:6379>?XGROUP?CREATE?queue?group2?0-0 OK

    消費者組創(chuàng)建好之后,我們可以給每個「消費者組」下面掛一個「消費者」,讓它們分別處理同一批數(shù)據(jù)。

    第一個消費組開始消費:

    //?group1的consumer開始消費,>表示拉取最新數(shù)據(jù) 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開始消費,>表示拉取最新數(shù)據(jù) 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 組消費者,都可以獲取同一批數(shù)據(jù)進行處理了。

    這樣一來,就達到了多組消費者「訂閱」消費的目的。

    3) 消息處理時異常,Stream 能否保證消息不丟失,重新消費?

    除了上面拉取消息時用到了消息 ID,這里為了保證重新消費,也要用到這個消息 ID。

    當一組消費者處理完消息后,需要執(zhí)行 XACK 命令告知 Redis,這時 Redis 就會把這條消息標記為「處理完成」。

    //?group1下的?1618472043089-0?消息已處理完成 127.0.0.1:6379>?XACK?queue?group1?1618472043089-0

    如果消費者異常宕機,肯定不會發(fā)送 XACK,那么 Redis 就會依舊保留這條消息。

    待這組消費者重新上線后,Redis 就會把之前沒有處理成功的數(shù)據(jù),重新發(fā)給這個消費者。這樣一來,即使消費者異常,也不會丟失數(shù)據(jù)了。

    //?消費者重新上線,0-0表示重新拉取未ACK的消息 127.0.0.1:6379>?XREADGROUP?GROUP?group1?consumer1?COUNT?5?STREAMS?queue?0-0 //?之前沒消費成功的數(shù)據(jù),依舊可以重新消費 1)?1)?"queue"2)?1)?1)?"1618472043089-0"2)?1)?"name"2)?"zhangsan"2)?1)?"1618472045158-0"2)?1)?"name"2)?"lisi"

    4) Stream 數(shù)據(jù)會寫入到 RDB 和 AOF 做持久化嗎?

    Stream 是新增加的數(shù)據(jù)類型,它與其它數(shù)據(jù)類型一樣,每個寫操作,也都會寫入到 RDB 和 AOF 中。

    我們只需要配置好持久化策略,這樣的話,就算 Redis 宕機重啟,Stream 中的數(shù)據(jù)也可以從 RDB 或 AOF 中恢復回來。

    5) 消息堆積時,Stream 是怎么處理的?

    其實,當消息隊列發(fā)生消息堆積時,一般只有 2 個解決方案:

  • 生產(chǎn)者限流:避免消費者處理不及時,導致持續(xù)積壓

  • 丟棄消息:中間件丟棄舊消息,只保留固定長度的新消息

  • 而 Redis 在實現(xiàn) Stream 時,采用了第 2 個方案。

    在發(fā)布消息時,你可以指定隊列的最大長度,防止隊列積壓導致內(nèi)存爆炸。

    //?隊列長度最大10000 127.0.0.1:6379>?XADD?queue?MAXLEN?10000?*?name?zhangsan "1618473015018-0"

    當隊列長度超過上限后,舊消息會被刪除,只保留固定長度的新消息。

    這么來看,Stream 在消息積壓時,如果指定了最大長度,還是有可能丟失消息的。

    除了以上介紹到的命令,Stream 還支持查看消息長度(XLEN)、查看消費者狀態(tài)(XINFO)等命令,使用也比較簡單,你可以查詢官方文檔了解一下,這里就不過多介紹了。

    好了,通過以上介紹,我們可以看到,Redis 的 Stream 幾乎覆蓋到了消息隊列的各種場景,是不是覺得很完美?

    既然它的功能這么強大,這是不是意味著,Redis 真的可以作為專業(yè)的消息隊列中間件來使用呢?

    但是還「差一點」,就算 Redis 能做到以上這些,也只是「趨近于」專業(yè)的消息隊列。

    原因在于 Redis 本身的一些問題,如果把其定位成消息隊列,還是有些欠缺的。

    到這里,就不得不把 Redis 與專業(yè)的隊列中間件做對比了。

    下面我們就來看一下,Redis 在作隊列時,到底還有哪些欠缺?

    與專業(yè)的消息隊列對比

    其實,一個專業(yè)的消息隊列,必須要做到兩大塊:

  • 消息不丟

  • 消息可堆積

  • 前面我們討論的重點,很大篇幅圍繞的是第一點展開的。

    這里我們換個角度,從一個消息隊列的「使用模型」來分析一下,怎么做,才能保證數(shù)據(jù)不丟?

    使用一個消息隊列,其實就分為三大塊:生產(chǎn)者、隊列中間件、消費者

    消息是否會發(fā)生丟失,其重點也就在于以下 3 個環(huán)節(jié):

  • 生產(chǎn)者會不會丟消息?

  • 消費者會不會丟消息?

  • 隊列中間件會不會丟消息?

  • 1) 生產(chǎn)者會不會丟消息?

    當生產(chǎn)者在發(fā)布消息時,可能發(fā)生以下異常情況:

  • 消息沒發(fā)出去:網(wǎng)絡故障或其它問題導致發(fā)布失敗,中間件直接返回失敗

  • 不確定是否發(fā)布成功:網(wǎng)絡問題導致發(fā)布超時,可能數(shù)據(jù)已發(fā)送成功,但讀取響應結(jié)果超時了

  • 如果是情況 1,消息根本沒發(fā)出去,那么重新發(fā)一次就好了。

    如果是情況 2,生產(chǎn)者沒辦法知道消息到底有沒有發(fā)成功?所以,為了避免消息丟失,它也只能繼續(xù)重試,直到發(fā)布成功為止。

    生產(chǎn)者一般會設(shè)定一個最大重試次數(shù),超過上限依舊失敗,需要記錄日志報警處理。

    也就是說,生產(chǎn)者為了避免消息丟失,只能采用失敗重試的方式來處理。

    但發(fā)現(xiàn)沒有?這也意味著消息可能會重復發(fā)送。

    是的,在使用消息隊列時,要保證消息不丟,寧可重發(fā),也不能丟棄。

    那消費者這邊,就需要多做一些邏輯了。

    對于敏感業(yè)務,當消費者收到重復數(shù)據(jù)數(shù)據(jù)時,要設(shè)計冪等邏輯,保證業(yè)務的正確性。

    從這個角度來看,生產(chǎn)者會不會丟消息,取決于生產(chǎn)者對于異常情況的處理是否合理。

    所以,無論是 Redis 還是專業(yè)的隊列中間件,生產(chǎn)者在這一點上都是可以保證消息不丟的。

    2) 消費者會不會丟消息?

    這種情況就是我們前面提到的,消費者拿到消息后,還沒處理完成,就異常宕機了,那消費者還能否重新消費失敗的消息?

    要解決這個問題,消費者在處理完消息后,必須「告知」隊列中間件,隊列中間件才會把標記已處理,否則仍舊把這些數(shù)據(jù)發(fā)給消費者。

    這種方案需要消費者和中間件互相配合,才能保證消費者這一側(cè)的消息不丟。

    無論是 Redis 的 Stream,還是專業(yè)的隊列中間件,例如 RabbitMQ、Kafka,其實都是這么做的。

    所以,從這個角度來看,Redis 也是合格的。

    3) 隊列中間件會不會丟消息?

    前面 2 個問題都比較好處理,只要客戶端和服務端配合好,就能保證生產(chǎn)端、消費端都不丟消息。

    但是,如果隊列中間件本身就不可靠呢?

    畢竟生產(chǎn)者和消費這都依賴它,如果它不可靠,那么生產(chǎn)者和消費者無論怎么做,都無法保證數(shù)據(jù)不丟。

    在這個方面,Redis 其實沒有達到要求。

    Redis 在以下 2 個場景下,都會導致數(shù)據(jù)丟失。

  • AOF 持久化配置為每秒寫盤,但這個寫盤過程是異步的,Redis 宕機時會存在數(shù)據(jù)丟失的可能

  • 主從復制也是異步的,主從切換時,也存在丟失數(shù)據(jù)的可能(從庫還未同步完成主庫發(fā)來的數(shù)據(jù),就被提成主庫)

  • 基于以上原因我們可以看到,Redis 本身的無法保證嚴格的數(shù)據(jù)完整性

    所以,如果把 Redis 當做消息隊列,在這方面是有可能導致數(shù)據(jù)丟失的。

    再來看那些專業(yè)的消息隊列中間件是如何解決這個問題的?

    像 RabbitMQ 或 Kafka 這類專業(yè)的隊列中間件,在使用時,一般是部署一個集群,生產(chǎn)者在發(fā)布消息時,隊列中間件通常會寫「多個節(jié)點」,以此保證消息的完整性。這樣一來,即便其中一個節(jié)點掛了,也能保證集群的數(shù)據(jù)不丟失。

    也正因為如此,RabbitMQ、Kafka在設(shè)計時也更復雜。畢竟,它們是專門針對隊列場景設(shè)計的。

    但 Redis 的定位則不同,它的定位更多是當作緩存來用,它們兩者在這個方面肯定是存在差異的。

    最后,我們來看消息積壓怎么辦?

    4) 消息積壓怎么辦?

    因為 Redis 的數(shù)據(jù)都存儲在內(nèi)存中,這就意味著一旦發(fā)生消息積壓,則會導致 Redis 的內(nèi)存持續(xù)增長,如果超過機器內(nèi)存上限,就會面臨被 OOM 的風險。

    所以,Redis 的 Stream 提供了可以指定隊列最大長度的功能,就是為了避免這種情況發(fā)生。

    但 Kafka、RabbitMQ 這類消息隊列就不一樣了,它們的數(shù)據(jù)都會存儲在磁盤上,磁盤的成本要比內(nèi)存小得多,當消息積壓時,無非就是多占用一些磁盤空間,相比于內(nèi)存,在面對積壓時也會更加「坦然」。

    綜上,我們可以看到,把 Redis 當作隊列來使用時,始終面臨的 2 個問題:

  • Redis 本身可能會丟數(shù)據(jù)

  • 面對消息積壓,Redis 內(nèi)存資源緊張

  • 到這里,Redis 是否可以用作隊列,我想這個答案你應該會比較清晰了。

    如果你的業(yè)務場景足夠簡單,對于數(shù)據(jù)丟失不敏感,而且消息積壓概率比較小的情況下,把 Redis 當作隊列是完全可以的。

    而且,Redis 相比于 Kafka、RabbitMQ,部署和運維也更加輕量。

    如果你的業(yè)務場景對于數(shù)據(jù)丟失非常敏感,而且寫入量非常大,消息積壓時會占用很多的機器資源,那么我建議你使用專業(yè)的消息隊列中間件。

    總結(jié)

    好了,總結(jié)一下。這篇文章我們從「Redis 能否用作隊列」這個角度出發(fā),介紹了 List、Pub/Sub、Stream 在做隊列的使用方式,以及它們各自的優(yōu)劣。

    之后又把 Redis 和專業(yè)的消息隊列中間件做對比,發(fā)現(xiàn) Redis 的不足之處。

    最后,我們得出 Redis 做隊列的合適場景。

    這里我也列了一個表格,總結(jié)了它們各自的優(yōu)缺點。

    ????????

    往期推薦

    為什么大家都在抵制用定時任務實現(xiàn)「關(guān)閉超時訂單」功能?

    Gartner 發(fā)布 2022 年汽車行業(yè)五大技術(shù)趨勢

    別再用 Redis List 實現(xiàn)消息隊列了,Stream 專為隊列而生

    OpenStack 如何跨版本升級

    點分享

    點收藏

    點點贊

    點在看

    總結(jié)

    以上是生活随笔為你收集整理的Redis 究竟适不适合当队列来用?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。