<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          把 Redis 當(dāng)作隊列來用,真的合適嗎?

          共 15335字,需瀏覽 31分鐘

           ·

          2021-06-02 22:28

          點擊關(guān)注公眾號,Java干貨及時送達

          轉(zhuǎn)自公眾號:水滴與銀彈

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

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

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

          究竟哪種方案更好呢?

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

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

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

          在文章的最后,我還會告訴你關(guān)于「技術(shù)選型」的思路,文章有點長,希望你可以耐心讀完。

          從最簡單的開始:List 隊列

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

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

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

          如果把 List 當(dāng)作隊列,你可以這么來用。

          生產(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"

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

          但這里有個小問題,當(dāng)隊列中已經(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 造成壓力。

          怎么解決這個問題呢?

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

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

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

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

          假設(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 會強制把這個客戶端踢下線。所以,采用這種方案,客戶端要有重連機制。

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

          我們一起來分析一下:

          1. 不支持重復(fù)消費:消費者拉取消息后,這條消息就從 List 中刪除了,無法被其它消費者再次消費,即不支持多個消費者消費同一批數(shù)據(jù)
          2. 消息丟失:消費者拉取到消息后,如果發(fā)生異常宕機,那這條消息就丟失了

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

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

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

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

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

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

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

          即多組生產(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è)務(wù)需求。

          除此之外,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ā)生以下場景,就有可能導(dǎo)致數(shù)據(jù)丟失:

          1. 消費者下線
          2. Redis 宕機
          3. 消息堆積

          究竟是怎么回事?

          這其實與 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ā)布、訂閱消息處理流程是這樣的:

          1. 消費者訂閱指定隊列,Redis 就會記錄一個映射關(guān)系:隊列->消費者
          2. 生產(chǎn)者向這個隊列發(fā)布消息,那 Redis 就從映射關(guān)系中找出對應(yīng)的消費者,把消息轉(zhuǎn)發(fā)給它

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

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

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

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

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

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

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

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

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

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

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

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

          這是怎么回事?

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

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

          當(dāng)生產(chǎn)者發(fā)布消息時,Redis 先把消息寫到對應(yīng)消費者的緩沖區(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 作隊列差異比較大的。

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

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

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

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

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

          1. 支持發(fā)布 / 訂閱,支持多組生產(chǎn)者、消費者處理消息
          2. 消費者下線,數(shù)據(jù)會丟失
          3. 不支持數(shù)據(jù)持久化,Redis 宕機,數(shù)據(jù)也會丟失
          4. 消息堆積,緩沖區(qū)溢出,消費者會被強制踢下線,數(shù)據(jù)也會丟失

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

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

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

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

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

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

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

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

          • 支持阻塞等待拉取消息
          • 支持發(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 是如何解決上面這些問題的。

          我們依舊從簡單到復(fù)雜,依次來看 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。

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

          // 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 中恢復(fù)回來。

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

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

          1. 生產(chǎn)者限流:避免消費者處理不及時,導(dǎo)致持續(xù)積壓
          2. 丟棄消息:中間件丟棄舊消息,只保留固定長度的新消息

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

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

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

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

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

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

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

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

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

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

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

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

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

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

          1. 消息不丟
          2. 消息可堆積

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

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

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

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

          1. 生產(chǎn)者會不會丟消息?
          2. 消費者會不會丟消息?
          3. 隊列中間件會不會丟消息?

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

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

          1. 消息沒發(fā)出去:網(wǎng)絡(luò)故障或其它問題導(dǎo)致發(fā)布失敗,中間件直接返回失敗
          2. 不確定是否發(fā)布成功:網(wǎng)絡(luò)問題導(dǎo)致發(fā)布超時,可能數(shù)據(jù)已發(fā)送成功,但讀取響應(yīng)結(jié)果超時了

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

          1. AOF 持久化配置為每秒寫盤,但這個寫盤過程是異步的,Redis 宕機時會存在數(shù)據(jù)丟失的可能
          2. 主從復(fù)制也是異步的,主從切換時,也存在丟失數(shù)據(jù)的可能(從庫還未同步完成主庫發(fā)來的數(shù)據(jù),就被提成主庫)

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

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

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

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

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

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

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

          4) 消息積壓怎么辦?

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

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

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

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

          1. Redis 本身可能會丟數(shù)據(jù)
          2. 面對消息積壓,Redis 內(nèi)存資源緊張

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

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

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

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

          總結(jié)

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

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

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

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

          后記

          最后,我想和你再聊一聊關(guān)于「技術(shù)方案選型」的問題。

          你應(yīng)該也看到了,這篇文章雖然始于 Redis,但并不止于 Redis。

          我們在分析 Redis 細節(jié)時,一直在提出問題,然后尋找更好的解決方案,在文章最后,又聊到一個專業(yè)的消息隊列應(yīng)該怎么做。

          其實,我們在討論技術(shù)選型時,就是一個關(guān)于如何取舍的問題。

          而這里我想傳達給你的信息是,在面對技術(shù)選型時,不要不經(jīng)過思考就覺得哪個方案好,哪個方案不好。

          你需要根據(jù)具體場景具體分析,這里我把這個分析過程分為 2 個層面:

          1. 業(yè)務(wù)功能角度
          2. 技術(shù)資源角度

          這篇文章所講到的內(nèi)容,都是以業(yè)務(wù)功能角度出發(fā)做決策的。

          但這里的第二點,從技術(shù)資源角度出發(fā),其實也很重要。

          技術(shù)資源的角度是說,你所處的公司環(huán)境、技術(shù)資源能否匹配這些技術(shù)方案。

          這個怎么解釋呢?

          簡單來講,就是你所在的公司、團隊,是否有匹配的資源能 hold 住這些技術(shù)方案。

          我們都知道 Kafka、RabbitMQ 是非常專業(yè)的消息中間件,但它們的部署和運維,相比于 Redis 來說,也會更復(fù)雜一些。

          如果你在一個大公司,公司本身就有優(yōu)秀的運維團隊,那么使用這些中間件肯定沒問題,因為有足夠優(yōu)秀的人能 hold 住這些中間件,公司也會投入人力和時間在這個方向上。

          但如果你是在一個初創(chuàng)公司,業(yè)務(wù)正處在快速發(fā)展期,暫時沒有能 hold 住這些中間件的團隊和人,如果貿(mào)然使用這些組件,當(dāng)發(fā)生故障時,排查問題也會變得很困難,甚至?xí)璧K業(yè)務(wù)的發(fā)展。

          而這種情形下,如果公司的技術(shù)人員對于 Redis 都很熟,綜合評估來看,Redis 也基本可以滿足業(yè)務(wù) 90% 的需求,那當(dāng)下選擇 Redis 未必不是一個好的決策。

          所以,做技術(shù)選型不只是技術(shù)問題,還與人、團隊、管理、組織結(jié)構(gòu)有關(guān)。

          也正是因為這些原因,當(dāng)你在和別人討論技術(shù)選型問題時,你會發(fā)現(xiàn)每個公司的做法都不相同。

          畢竟每個公司所處的環(huán)境和文化不一樣,做出的決策當(dāng)然就會各有差異。

          如果你不了解這其中的邏輯,那在做技術(shù)選型時,只會趨于表面現(xiàn)象,無法深入到問題根源。

          而一旦你理解了這個邏輯,那么你在看待這個問題時,不僅對于技術(shù)會有更加深刻認識,對技術(shù)資源和人的把握,也會更加清晰。

          希望你以后在做技術(shù)選型時,能夠把這些因素也考慮在內(nèi),這對你的技術(shù)成長之路也是非常有幫助的。






          關(guān)注Java技術(shù)棧看更多干貨



          獲取 Spring Boot 實戰(zhàn)筆記!
          瀏覽 55
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  香蕉伊人在线 | 一级a作爱视频 | www.操| 伊人在线久久 | 影音先锋男人在线资源 |