淺談消息隊列以及面試題

1.爲什麼要使用消息隊列

分析:一個用消息隊列的人,不知道爲啥用,這就有點尷尬、沒有複習這點,很容易被問蒙,然後就開始胡扯了。

回答:這個問題,咱只答三個最主要的應用場景(不可否認還有掐的,但是隻答三個主要的),即以下六個字:解耦、異步、削峯

(1)解耦
傳統模式:

 

傳統模式的缺點:

  • 系統間耦合性太強,如上圖所示,系統A在代碼中直接調用系統B和系統C的代碼,如果將來D系統接入,系統A還需要修改代碼,過於麻煩!

中間件模式:

 

中間件模式的優點:

  • 將消息寫入消息隊列,需要消息的系統自己從消息隊列中訂閱,從而系統A不需要做任何修改。

(2)異步
傳統模式:

 


傳統模式的缺點:

  • 一些非必要的業務邏輯以同步的方式運行,太耗費時間。

中間件模式:

 

中間件模式的優點:

  • 將消息寫入消息隊列,非必要的業務邏輯以異步的方式運行,加快相應速度

(3)削峯
傳統模式:

 

 

傳統模式的缺點:

  • 併發量大的時間,所有的請求直接懟到數據庫,造成數據庫連接異常

中間件模式:

 

中間件模式的優點:

  • 系統A慢慢的按照數據庫能處理的併發量,從消息隊列中慢慢拉取消息。在生產中,這個短暫的高峯期積壓是允許的。

2.使用了消息隊列會有什麼缺點

分析:一個使用了MQ的項目,如果連這個問題都沒有考慮過,就把MQ引進來了,那就給自己的項目帶來了風險。我們引入一個技術,要對這個技術的弊端有充分的認識,才能做好預防。要記住,不要給公司挖坑!

回答:可以從連個角度來思考

  • 系統可用性降低:你想呀,本來其他系統只要運行好好的,那你的系統就是正常的。現在你非要加入個消息隊列進去,那消息隊列掛了,你的系統不是呵呵了。因此,系統可用性會降低
  • 系統複雜性增加:加入了消息隊列,要多考慮很多方面的問題,比如:一致性問題、如何保證消息不被重複消費、如何保證消息可靠性傳輸等。因此,需要考慮的東西更多,刺痛複雜性增大。

但是,我們該用的還是要用的。

3.消息隊列如何選型?

先交個底,博主只會ActiveMQ,RabbitMQ,RocketMQ,Kafka,對什麼ZeroMQ或其他的MQ沒啥理解,因此只能基於這四種MQ給出回答。

分析:既然在項目中用了MQ,肯定事先要對業界流行的MQ進行調研,如果連每種MQ的優缺點都沒沒了解清楚,就拍腦袋依據喜好來選用某種MQ,這是給項目挖坑。如果面試官問:你爲什麼用這種MQ?你直接回答:領導決定的。這種回答就是很LOW。還是那句話,不要給公司挖坑。

回答:首先,咱先上ActiveMQ的社區,看看該MQ的更新頻率:

Apache ActiveMQ 5.15.3 Release
Christopher L. Shannon posted on Feb 12, 2018
Apache ActiveMQ 5.15.2 Released
Christopher L. Shannon posted on Oct 23, 2017
Apache ActiveMQ 5.15.0 Released
Christopher L. Shannon posted on Jul 06, 2017
省略以下記錄

我們可以看出,ActiveMQ幾個月才發一次版本,據說研究重心在他們的下一代產品Apollo。

接下來,我們再去RabbitMQ的社區去看一下,RabbitMQ的更新頻率

RabbitMQ 3.7.3 release  30 January 2018
RabbitMQ 3.6.15 release  17 January 2018
RabbitMQ 3.7.2 release23 December 2017
RabbitMQ 3.7.1 release21 December 2017
省略以下記錄
...

我們可以看出,RabbitMQ版本發佈比ActiveMq頻繁很多。至於RocketMQ和kafka就不帶大家看了,總之也比ActiveMQ活躍的多。詳情,可自行查閱。

特性 ActiveMQ RabbitMQ RocketMQ kafka
開發語言 java erlang java scala
單機吞吐量 萬級 萬級 10萬級 10萬級
時效性 ms級 us級 ms級 ms級以內
可用性 高(主從架構) 高(主從架構) 非常高(分佈式架構) 非常高(分佈式架構)
功能特性 成熟的產品,在很多公司得到應用;有較多的文檔;各種協議支持較好 基於erlang開發,所以併發能力很強,性能極其好,延時很低;管理界面較豐富 MQ功能比較完備,擴展性佳 只支持主要的MQ功能,像一些消息查詢,消息回溯等功能沒有提供,畢竟是爲大數據準備的,在大數據領域應用廣。

綜合上面的材料得出以下兩點:
(1)中小型軟件公司,建議選RabbitMQ.一方面,erlang語言天生具備高併發的特性,而且他的管理界面用起來十分方便。正所謂,成也蕭何,敗也蕭何!他的弊端也在這裏,雖然RabbitMQ是開源的,然而國內有幾個能定製化開發erlang的程序員呢?所幸,RabbitMQ的社區十分活躍,可以解決開發過程中遇到的bug,這點對於中小型公司來說十分重要。不考慮rocketmq和kafka的原因是,一方面中小型軟件公司不如互聯網公司,數據量沒那麼大,選消息中間件,應首選功能比較完備的,所以kafka排除。不考慮rocketmq的原因是,rocketmq是阿里出品,如果阿里放棄維護rocketmq,中小型公司一般抽不出人來進行rocketmq的定製化開發,因此不推薦。

(2)大型軟件公司,根據具體使用在rocketMq和kafka之間二選一。一方面,大型軟件公司,具備足夠的資金搭建分佈式環境,也具備足夠大的數據量。針對rocketMQ,大型軟件公司也可以抽出人手對rocketMQ進行定製化開發,畢竟國內有能力改JAVA源碼的人,還是相當多的。至於kafka,根據業務場景選擇,如果有日誌採集功能,肯定是首選kafka了。具體該選哪個,看使用場景。

4.如何保證消息隊列是高可用的

分析:在第二點說過了,引入消息隊列後,系統的可用性下降。在生產中,沒人使用單機模式的消息隊列。因此,一個合格的程序員,應該對消息隊列的高可用有很深刻的瞭解。如果面試的時候,面試官問,你們的消息隊列是如何保證高可用的?你的回答只是表明自己只會訂閱和發佈消息,面試官就會懷疑你是不是隻是自己搭着完,壓根沒在生產用過。請做一個愛思考,會思考,懂思考的程序員。

回答:這問題,其實要對消息隊列的集羣模式要有深刻了解,纔好回答。

以RocketMQ爲例:他的集羣就有多master模式、多master多slave異步複製模式、多master多slave同步雙寫模式。多master多slave模式部署架構圖(圖網上找的,偷個懶,懶得畫):

 

其實博主第一眼看到這個圖,就覺得和Kafka好像,只是NameServer集羣,就在Kafka中是用zookeeper代替,都是用來保存和發現master和slave用的。通信過程如下:

Producer 與 NameServer集羣中的其中一個節點(隨機選擇)建立長連接,定期從 NameServer 獲取 Topic 路由信息,並向提供 Topic 服務的 Broker Master 建立長連接,且定時向 Broker 發送心跳。Producer 只能將消息發送到 Broker master,但是 Consumer 則不一樣,它同時和提供 Topic 服務的 Master 和 Slave建立長連接,既可以從 Broker Master 訂閱消息,也可以從 Broker Slave 訂閱消息。

那麼kafka呢,爲了對比說明,直接上kafka的拓撲架構圖(也是找的,懶得畫)

 

如上圖所示,一個典型的Kafka集羣中包含若干Producer(可以是web前端產生的Page View,或者是服務器日誌,系統CPU、Memory等),若干broker(Kafka支持水平擴展,一般broker數量越多,集羣吞吐率越高),若干Consumer Group,以及一個Zookeeper集羣。Kafka通過Zookeeper管理集羣配置,選舉leader,以及在Consumer Group發生變化時進行rebalance。Producer使用push模式將消息發佈到broker,Consumer使用pull模式從broker訂閱並消費消息。
至於rabbitMQ,也有普通集羣和鏡像集羣模式,自行去了解,比較簡單,兩小時即懂。

要求,在回答高可用的問題時,應該能邏輯清晰的畫出自己的MQ集羣架構或清晰的敘述出來。

5.如何保證消息不被重複消費

分析:這個問題其實換一種問法就是,如何保證消息隊列的冪等性?這個問題可以認爲是消息隊列領域的基本問題。換句話來說,是在考察你的設計能力,這個問題的回答可以根據具體的業務場景來答,沒有固定的答案。

回答:先來說一下爲什麼會造成重複消費?
其實無論是哪種消息隊列,造成重複消費原因其實都是類似的。正常情況下,消費者在消費消息的時候,消費完畢後,會發送一個確認消息給消息隊列,消息隊列就知道該消息被消費了,就會將該消息從消息隊列中刪除。只是不同的消息隊列發出的確認消息形式不同,例如RabbitMQ是發送一個ACK確認消息,RocketMQ是返回一個CONSUME_SUCCESS成功標誌,kafka實際上有個offet的概念,簡單說一下,就是每一個消息都有一個offset,kafka消費過消息後,需要提交offset,讓消息隊列知道自己已經消費過了。

那造成重複消費的原因?,就是因爲網絡傳輸等等故障,確認信息沒有傳送到消息隊列,導致消息隊列不知道自己已經消費過該消息了,再次將消息分發給其他的消費者。

如何解決?這個問題針對業務場景來答,分以下三種情況:

(1)比如,你拿到這個消息做數據庫的insert操作,那就容易了,給這個消息做一個唯一的主鍵,那麼就算出現重複消費的情況,就會導致主鍵衝突,避免數據庫出現髒數據。

(2)再比如,你拿到這個消息做redis的set的操作,那就容易了,不用解決,因爲你無論set幾次結果都是一樣的,set操作本來就算冪等操作。

(3)如果上面兩種情況還不行,上大招。準備一個第三方介質,來做消費記錄。以redis爲例,給消息分配一個全局id,只要消費過該消息,將<id,message>以K-V形式寫入redis.那消費者開始消費前,先去redis中查詢有沒有消費記錄即可。

6.如何保證消費的可靠性傳輸?

分析:我們在使用消息隊列的過程中,應該做到消息不能多消費,也不能少消費。如果無法做到可靠性傳輸,可能給公司帶來千萬級別的財產損失。同樣的,如果可靠性傳輸在使用過程中,沒有考慮到,這不是給公司挖坑麼,你可以拍拍屁股走人,公司損失的錢,誰承擔。還是那句話,認真對待每一個項目,不要給公司挖坑。

回答:其實這個可靠性傳輸,每種MQ都要從三個角度來分析:

  • 生產者弄丟數據
  • 消息隊列弄丟數據
  • 消費者弄丟數據

(1)生產者丟數據
從生產者弄丟數據這個角度來看,RabbitMQ提供transaction和confirm模式來確保生產者不丟消息。

transaction機制就是說,發送消息前,開啓事務(channel.txSelect()),然後發送消息,如果發送過程中出現什麼異常,事務就會回滾(channel.txRollback()),如果發送成功則提交事務(channel.txCommit())。

然而,這種方式有個缺點:吞吐量下降。因爲,按照經驗,生產上用confirm模式的居多。一旦channel進入confirm模式,所有在該信道上發佈的消息都將會被指派一個唯一的ID(從1開始),一旦消息被投遞到所有匹配的隊列之後,rabbitMQ就會發送一個ACK給生產者(包含消息的唯一ID),這就使得生產者知道消息已經正確到達目的隊列了。如果rabbitMQ沒能處理該消息,則會發送一個Nack消息給你,你可以進行重試操作。處理Ack和Nack的代碼如下所示:

channel.addConfirmListener(new ConfirmListener() {  
                @Override  
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {  
                    System.out.println("nack: deliveryTag = "+deliveryTag+" multiple: "+multiple);  
                }  
                @Override  
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {  
                    System.out.println("ack: deliveryTag = "+deliveryTag+" multiple: "+multiple);  
                }  
            });  

(2)消息隊列丟數據

處理消息隊列丟數據的情況,一般是開啓持久化磁盤的配置。這個持久化配置可以和confirm機制配合使用,你可以在消息持久化磁盤後,再給生產者發送一個Ack信號。這樣,如果消息持久化磁盤之前,rabbitMQ陣亡了,那麼生產者收不到Ack信號,生產者會自動重發。

那麼如何持久化呢,這裏順便說一下吧,其實也很容易,就下面兩步

  1. 將queue的持久化標識durable設置爲true,則代表是一個持久的隊列
  2. 發送消息的時候將deliveryMode=2

這樣設置以後,即使rabbitMQ掛了,重啓後也能恢復數據

(3)消費者丟數據

消費者丟數據一般是因爲採用了自動確認消息模式。這種模式下,消費者會自動確認收到信息。這時rabbitMQ會立即將消息刪除,這種情況下,如果消費者出現異常而未能處理消息,就會丟失該消息。

至於解決方案,採用手動確認消息即可。

7.如何保證消息的順序性

分析:其實並非所有的公司都有這種業務需求,但是還是對這個問題要有所複習。

回答:針對這個問題,通過某種算法,將需要保持先後順序的消息放到同一個消息隊列中(kafka中就是partition,rabbitMq中就是queue)。然後只用一個消費者去消費該隊列。
有的人會問:那如果爲了吞吐量,有多個消費者去消費怎麼辦?

這個問題,沒有固定回答的套路。比如我們有一個微博的操作,發微博、寫評論、刪除微博,這三個異步操作。如果是這樣一個業務場景,那隻要重試就行。比如你一個消費者先執行了寫評論的操作,但是這時候,微博都還沒發,寫評論一定是失敗的,等一段時間。等另一個消費者,先執行寫評論的操作後,再執行,就可以成功。

總之,針對這個問題,我的觀點是保證入隊有序就行,出隊以後的順序交給消費者自己去保證,沒有固定套路。

8.JMS VS AMQP

 JMS

 JMS 簡介

JMS(JAVA Message Service,java消息服務)是java的消息服務,JMS的客戶端之間可以通過JMS服務進行異步的消息傳輸。JMS(JAVA Message Service,Java消息服務)API是一個消息服務的標準或者說是規範,允許應用程序組件基於JavaEE平臺創建、發送、接收和讀取消息。它使分佈式通信耦合度更低,消息服務更加可靠以及異步性。

ActiveMQ 就是基於 JMS 規範實現的。

 JMS兩種消息模型

①點到點(P2P)模型

點到點(P2P)模型

使用隊列(Queue)作爲消息通信載體;滿足生產者與消費者模式,一條消息只能被一個消費者使用,未被消費的消息在隊列中保留直到被消費或超時。比如:我們生產者發送100條消息的話,兩個消費者來消費一般情況下兩個消費者會按照消息發送的順序各自消費一半(也就是你一個我一個的消費。)

② 發佈/訂閱(Pub/Sub)模型

發佈/訂閱(Pub/Sub)模型

發佈訂閱模型(Pub/Sub) 使用主題(Topic)作爲消息通信載體,類似於廣播模式;發佈者發佈一條消息,該消息通過主題傳遞給所有的訂閱者,在一條消息廣播之後才訂閱的用戶則是收不到該條消息的

JMS 五種不同的消息正文格式

JMS定義了五種不同的消息正文格式,以及調用的消息類型,允許你發送並接收以一些不同形式的數據,提供現有消息格式的一些級別的兼容性。

  • StreamMessage -- Java原始值的數據流
  • MapMessage--一套名稱-值對
  • TextMessage--一個字符串對象
  • ObjectMessage--一個序列化的 Java對象
  • BytesMessage--一個字節的數據流

 AMQP

​ AMQP,即Advanced Message Queuing Protocol,一個提供統一消息服務的應用層標準 高級消息隊列協議(二進制應用層協議),是應用層協議的一個開放標準,爲面向消息的中間件設計,兼容 JMS。基於此協議的客戶端與消息中間件可傳遞消息,並不受客戶端/中間件同產品,不同的開發語言等條件的限制。

RabbitMQ 就是基於 AMQP 協議實現的。

4.3 JMS vs AMQP

對比方向 JMS AMQP
定義 Java API 協議
跨語言
跨平臺
支持消息類型 提供兩種消息模型:①Peer-2-Peer;②Pub/sub 提供了五種消息模型:①direct exchange;②fanout exchange;③topic change;④headers exchange;⑤system exchange。本質來講,後四種和JMS的pub/sub模型沒有太大差別,僅是在路由機制上做了更詳細的劃分;
支持消息類型 支持多種消息類型 ,我們在上面提到過 byte[](二進制)

總結:

  • AMQP 爲消息定義了線路層(wire-level protocol)的協議,而JMS所定義的是API規範。在 Java 體系中,多個client均可以通過JMS進行交互,不需要應用修改代碼,但是其對跨平臺的支持較差。而AMQP天然具有跨平臺、跨語言特性。
  • JMS 支持TextMessage、MapMessage 等複雜的消息類型;而 AMQP 僅支持 byte[] 消息類型(複雜的類型可序列化後發送)。
  • 由於Exchange 提供的路由算法,AMQP可以提供多樣化的路由方式來傳遞消息到消息隊列,而 JMS 僅支持 隊列 和 主題/訂閱 方式兩種。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章