什麼是訊息佇列?

Java3y發表於2019-04-12

前言

只有光頭才能變強。

文字已收錄至我的GitHub倉庫,歡迎Star:github.com/ZhongFuChen…

公司用到的很多技術,自己之前都沒學過(),於是只能慢慢補了。這次給大家寫寫我學習訊息佇列的筆記,希望對大家有幫助。

一、什麼是訊息佇列?

訊息佇列不知道大家看到這個詞的時候,會不會覺得它是一個比較高階的技術,反正我是覺得它好像是挺牛逼的。

訊息佇列,一般我們會簡稱它為MQ(Message Queue),嗯,就是很直白的簡寫。

我們先不管訊息(Message)這個詞,來看看佇列(Queue)。這一看,佇列大家應該都熟悉吧。

佇列是一種先進先出的資料結構。

先進先出

在Java裡邊,已經實現了不少的佇列了:

Java的佇列實現類

那為什麼還需要訊息佇列(MQ)這種中介軟體呢???其實這個問題,跟之前我學Redis的時候很像。Redis是一個以key-value形式儲存的記憶體資料庫,明明我們可以使用類似HashMap這種實現類就可以達到類似的效果了,那還為什麼要Redis?《Redis合集

  • 到這裡,大家可以先猜猜為什麼要用訊息佇列(MQ)這種中介軟體,下面會繼續補充。

訊息佇列可以簡單理解為:把要傳輸的資料放在佇列中

圖片來源:https://www.cloudamqp.com/blog/2014-12-03-what-is-message-queuing.html
)

科普:

  • 把資料放到訊息佇列叫做生產者
  • 從訊息佇列裡邊取資料叫做消費者

二、為什麼要用訊息佇列?

為什麼要用訊息佇列,也就是在問:用了訊息佇列有什麼好處。我們看看以下的場景

2.1 解耦

現在我有一個系統A,系統A可以產生一個userId

系統A可以產生一個UserId

然後,現在有系統B和系統C都需要這個userId去做相關的操作

系統A給系統B和系統C傳入userId這個值

寫成虛擬碼可能是這樣的:

public class SystemA {

    // 系統B和系統C的依賴
    SystemB systemB = new SystemB();
    SystemC systemC = new SystemC();

    // 系統A獨有的資料userId
    private String userId = "Java3y";

    public void doSomething() {

        // 系統B和系統C都需要拿著系統A的userId去操作其他的事
        systemB.SystemBNeed2do(userId);
        systemC.SystemCNeed2do(userId);
        
    }
}
複製程式碼

結構圖如下:

結構圖

ok,一切平安無事度過了幾個天。

某一天,系統B的負責人告訴系統A的負責人,現在系統B的SystemBNeed2do(String userId)這個介面不再使用了,讓系統A別去調它了

於是,系統A的負責人說"好的,那我就不呼叫你了。",於是就把呼叫系統B介面的程式碼給刪掉了

public void doSomething() {

  // 系統A不再呼叫系統B的介面了
  //systemB.SystemBNeed2do(userId);
  systemC.SystemCNeed2do(userId);

}
複製程式碼

又過了幾天,系統D的負責人接了個需求,也需要用到系統A的userId,於是就跑去跟系統A的負責人說:"老哥,我要用到你的userId,你調一下我的介面吧"

於是系統A說:"沒問題的,這就搞"

系統A需要呼叫系統D的介面

然後,系統A的程式碼如下:

public class SystemA {

    // 已經不再需要系統B的依賴了
    // SystemB systemB = new SystemB();
    
    // 系統C和系統D的依賴
    SystemC systemC = new SystemC();
    SystemD systemD = new SystemD();

    // 系統A獨有的資料
    private String userId = "Java3y";

    public void doSomething() {

       
        // 已經不再需要系統B的依賴了
        //systemB.SystemBNeed2do(userId);

        // 系統C和系統D都需要拿著系統A的userId去操作其他的事
        systemC.SystemCNeed2do(userId);
        systemD.SystemDNeed2do(userId);

    }
}
複製程式碼

時間飛逝:

  • 又過了幾天,系統E的負責人過來了,告訴系統A,需要userId。
  • 又過了幾天,系統B的負責人過來了,告訴系統A,還是重新掉那個介面吧。
  • 又過了幾天,系統F的負責人過來了,告訴系統A,需要userId。
  • …...

於是系統A的負責人,每天都被這給騷擾著,改來改去,改來改去.......

還有另外一個問題,呼叫系統C的時候,如果系統C掛了,系統A還得想辦法處理。如果呼叫系統D時,由於網路延遲,請求超時了,那系統A是反饋fail還是重試??

最後,系統A的負責人,覺得隔一段時間就改來改去,沒意思,於是就跑路了。

然後,公司招來一個大佬,大佬經過幾天熟悉,上來就說:將系統A的userId寫到訊息佇列中,這樣系統A就不用經常改動了。為什麼呢?下面我們來一起看看:

系統A將userId寫到訊息佇列中,系統C和系統D從訊息佇列中拿資料

系統A將userId寫到訊息佇列中,系統C和系統D從訊息佇列中拿資料。這樣有什麼好處

  • 系統A只負責把資料寫到佇列中,誰想要或不想要這個資料(訊息),系統A一點都不關心

    • 即便現在系統D不想要userId這個資料了,系統B又突然想要userId這個資料了,都跟系統A無關,系統A一點程式碼都不用改。
  • 系統D拿userId不再經過系統A,而是從訊息佇列裡邊拿。系統D即便掛了或者請求超時,都跟系統A無關,只跟訊息佇列有關

這樣一來,系統A與系統B、C、D都解耦了。

2.2 非同步

我們再來看看下面這種情況:系統A還是直接呼叫系統B、C、D

直接調介面

程式碼如下:

public class SystemA {

    SystemB systemB = new SystemB();
    SystemC systemC = new SystemC();
    SystemD systemD = new SystemD();

    // 系統A獨有的資料
    private String userId ;

    public void doOrder() {
     
        // 下訂單
      	userId = this.order();
        // 如果下單成功,則安排其他系統做一些事  
        systemB.SystemBNeed2do(userId);
        systemC.SystemCNeed2do(userId);
        systemD.SystemDNeed2do(userId);

    }
}

複製程式碼

假設系統A運算出userId具體的值需要50ms,呼叫系統B的介面需要300ms,呼叫系統C的介面需要300ms,呼叫系統D的介面需要300ms。那麼這次請求就需要50+300+300+300=950ms

並且我們得知,系統A做的是主要的業務,而系統B、C、D是非主要的業務。比如系統A處理的是訂單下單,而系統B是訂單下單成功了,那傳送一條簡訊告訴具體的使用者此訂單已成功,而系統C和系統D也是處理一些小事而已。

那麼此時,為了提高使用者體驗和吞吐量,其實可以非同步地呼叫系統B、C、D的介面。所以,我們可以弄成是這樣的:

此時才用了100ms

系統A執行完了以後,將userId寫到訊息佇列中,然後就直接返回了(至於其他的操作,則非同步處理)。

  • 本來整個請求需要用950ms(同步)
  • 現在將呼叫其他系統介面非同步化,只需要100ms(非同步)

(例子可能舉得不太好,但我覺得說明到點子上就行了,見諒。)

2.3削峰/限流

我們再來一個場景,現在我們每個月要搞一次大促,大促期間的併發可能會很高的,比如每秒3000個請求。假設我們現在有兩臺機器處理請求,並且每臺機器只能每次處理1000個請求。

削峰的場景

那多出來的1000個請求,可能就把我們整個系統給搞崩了...所以,有一種辦法,我們可以寫到訊息佇列中:

寫到訊息佇列中,系統從訊息佇列中拿到請求

系統B和系統C根據自己的能夠處理的請求數去訊息佇列中拿資料,這樣即便有每秒有8000個請求,那只是把請求放在訊息佇列中,去拿訊息佇列的訊息由系統自己去控制,這樣就不會把整個系統給搞崩。

三、使用訊息佇列有什麼問題?

經過我們上面的場景,我們已經可以發現,訊息佇列能做的事其實還是蠻多的。

說到這裡,我們先回到文章的開頭,"明明JDK已經有不少的佇列實現了,我們還需要訊息佇列中介軟體呢?"其實很簡單,JDK實現的佇列種類雖然有很多種,但是都是簡單的記憶體佇列。為什麼我說JDK是簡單的記憶體佇列呢?下面我們來看看要實現訊息佇列(中介軟體)可能要考慮什麼問題

3.1高可用

無論是我們使用訊息佇列來做解耦、非同步還是削峰,訊息佇列肯定不能是單機的。試著想一下,如果是單機的訊息佇列,萬一這臺機器掛了,那我們整個系統幾乎就是不可用了。

萬一單機的佇列掛掉了

所以,當我們專案中使用訊息佇列,都是得叢集/分散式的。要做叢集/分散式就必然希望該訊息佇列能夠提供現成的支援,而不是自己寫程式碼手動去實現。

3.2 資料丟失問題

我們將資料寫到訊息佇列上,系統B和C還沒來得及取訊息佇列的資料,就掛掉了。如果沒有做任何的措施,我們的資料就丟了

資料丟失問題

學過Redis的都知道,Redis可以將資料持久化磁碟上,萬一Redis掛了,還能從磁碟從將資料恢復過來。同樣地,訊息佇列中的資料也需要存在別的地方,這樣才儘可能減少資料的丟失。

那存在哪呢?

  • 磁碟?
  • 資料庫?
  • Redis?
  • 分散式檔案系統?

同步儲存還是非同步儲存?

3.3消費者怎麼得到訊息佇列的資料?

消費者怎麼從訊息佇列裡邊得到資料?有兩種辦法:

  • 生產者將資料放到訊息佇列中,訊息佇列有資料了,主動叫消費者去拿(俗稱push)
  • 消費者不斷去輪訓訊息佇列,看看有沒有新的資料,如果有就消費(俗稱pull)

3.4其他

除了這些,我們在使用的時候還得考慮各種的問題:

  • 訊息重複消費了怎麼辦啊?
  • 我想保證訊息是絕對有順序的怎麼做?
  • ……..

雖然訊息佇列給我們帶來了那麼多的好處,但同時我們發現引入訊息佇列也會提高系統的複雜性。市面上現在已經有不少訊息佇列輪子了,每種訊息佇列都有自己的特點,選取哪種MQ還得好好斟酌

最後

本文主要講解了什麼是訊息佇列,訊息佇列可以為我們帶來什麼好處,以及一個訊息佇列可能會涉及到哪些問題。希望給大家帶來一定的幫助。

參考資料:

樂於輸出乾貨的Java技術公眾號:Java3y。公眾號內有200多篇原創技術文章、海量視訊資源、精美腦圖,不妨來關注一下!

帥的人都關注了

覺得我的文章寫得不錯,不妨點一下

相關文章