RabbitMQ常見面試題

一. 使用RabbitMQ的好處
1.解耦,系統A在代碼中直接調用系統B和系統C的代碼,若是未來D系統接入,系統A還須要修改代碼,過於麻煩!
2.異步,將消息寫入消息隊列,非必要的業務邏輯以異步的方式運行,加快響應速度
3.削峯,併發量大的時候,全部的請求直接懟到數據庫,形成數據庫鏈接異常
2、RabbitMQ 中的 broker 是指什麼?cluster 又是指什麼?java

  1. broker 是指一個或多個 erlang node 的邏輯分組,且 node 上運行着 RabbitMQ 應用程序。cluster 是在 broker 的基礎之上,增長了 node 之間共享元數據的約束。

3、RabbitMQ 概念裏的 channel、exchange 和 queue 是邏輯概念,仍是對應着進程實體?分別起什麼做用?node

  1. queue 具備本身的 erlang 進程;exchange 內部實現爲保存 binding 關係的查找表;channel 是實際進行路由工做的實體,即負責按照 routing_key 將 message 投遞給 queue 。由 AMQP 協議描述可知,channel 是真實 TCP 鏈接之上的虛擬鏈接,全部 AMQP 命令都是經過 channel 發送的,且每個 channel 有惟一的 ID。一個 channel 只能被單獨一個操做系統線程使用,故投遞到特定 channel 上的 message 是有順序的。但一個操做系統線程上容許使用多個 channel 。

4、vhost 是什麼?起什麼做用?面試

  1. vhost 能夠理解爲虛擬 broker ,即 mini-RabbitMQ  server。其內部均含有獨立的 queue、exchange 和 binding 等,但最最重要的是,其擁有獨立的權限系統,能夠作到 vhost 範圍的用戶控制。固然,從 RabbitMQ 的全局角度,vhost 能夠做爲不一樣權限隔離的手段(一個典型的例子就是不一樣的應用能夠跑在不一樣的 vhost 中)。

5、消息基於什麼傳輸?redis

  1. 因爲TCP鏈接的建立和銷燬開銷較大,且併發數受系統資源限制,會形成性能瓶頸。RabbitMQ使用信道的方式來傳輸數據。信道是創建在真實的TCP鏈接內的虛擬鏈接,且每條TCP鏈接上的信道數量沒有限制。

6、消息如何分發?數據庫

  1. 若該隊列至少有一個消費者訂閱,消息將以循環(round-robin)的方式發送給消費者。每條消息只會分發給一個訂閱的消費者(前提是消費者可以正常處理消息並進行確認)。

7、消息怎麼路由?安全

  • 從概念上來講,消息路由必須有三部分:交換器、路由、綁定。生產者把消息發佈到交換器上;綁定決定了消息如何從路由器路由到特定的隊列;消息最終到達隊列,並被消費者接收。
  • 消息發佈到交換器時,消息將擁有一個路由鍵(routing key),在消息建立時設定。
  • 經過隊列路由鍵,能夠把隊列綁定到交換器上。
  • 消息到達交換器後,RabbitMQ會將消息的路由鍵與隊列的路由鍵進行匹配(針對不一樣的交換器有不一樣的路由規則)。若是可以匹配到隊列,則消息會投遞到相應隊列中;若是不能匹配到任何隊列,消息將進入 「黑洞」。
  • 經常使用的交換器主要分爲一下三種:
  • direct:若是路由鍵徹底匹配,消息就被投遞到相應的隊列
  • fanout:若是交換器收到消息,將會廣播到全部綁定的隊列上
  • topic:可使來自不一樣源頭的消息可以到達同一個隊列。 使用topic交換器時,可使用通配符,好比:「*」 匹配特定位置的任意文本, 「.」 把路由鍵分爲了幾部分,「#」 匹配全部規則等。特別注意:發往topic交換器的消息不能隨意的設置選擇鍵(routing_key),必須是由"."隔開的一系列的標識符組成。

8、什麼是元數據?元數據分爲哪些類型?包括哪些內容?與 cluster 相關的元數據有哪些?元數據是如何保存的?元數據在 cluster 中是如何分佈的?併發

  • 在非 cluster 模式下,元數據主要分爲 Queue 元數據(queue 名字和屬性等)、Exchange 元數據(exchange 名字、類型和屬性等)、Binding 元數據(存放路由關係的查找表)、Vhost 元數據(vhost 範圍內針對前三者的名字空間約束和安全屬性設置)。在 cluster 模式下,還包括 cluster 中 node 位置信息和 node 關係信息。元數據按照 erlang node 的類型肯定是僅保存於 RAM 中,仍是同時保存在 RAM 和 disk 上。元數據在 cluster 中是全 node 分佈的。
    下圖所示爲 queue 的元數據在單 node 和 cluster 兩種模式下的分佈圖。
    RabbitMQ常見面試題

9、在單 node 系統和多 node 構成的 cluster 系統中聲明 queue、exchange ,以及進行 binding 會有什麼不一樣?異步

  • 答:當你在單 node 上聲明 queue 時,只要該 node 上相關元數據進行了變動,你就會獲得 Queue.Declare-ok 迴應;而在 cluster 上聲明 queue ,則要求 cluster 上的所有 node 都要進行元數據成功更新,纔會獲得 Qu6eue.Declare-ok 迴應。另外,若 node 類型爲 RAM node 則變動的數據僅保存在內存中,若類型爲 disk node 則還要變動保存在磁盤上的數據。ide

  • 死信隊列&死信交換器:DLX 全稱(Dead-Letter-Exchange),稱之爲死信交換器,當消息變成一個死信以後,若是這個消息所在的隊列存在x-dead-letter-exchange參數,那麼它會被髮送到x-dead-letter-exchange對應值的交換器上,這個交換器就稱之爲死信交換器,與這個死信交換器綁定的隊列就是死信隊列。

10、如何確保消息正確地發送至RabbitMQ?性能

  • RabbitMQ使用發送方確認模式,確保消息正確地發送到RabbitMQ。發送方確認模式:將信道設置成confirm模式(發送方確認模式),則全部在信道上發佈的消息都會被指派一個惟一的ID。一旦消息被投遞到目的隊列後,或者消息被寫入磁盤後(可持久化的消息),信道會發送一個確認給生產者(包含消息惟一ID)。若是RabbitMQ發生內部錯誤從而致使消息丟失,會發送一條nack(not acknowledged,未確認)消息。發送方確認模式是異步的,生產者應用程序在等待確認的同時,能夠繼續發送消息。當確認消息到達生產者應用程序,生產者應用程序的回調方法就會被觸發來處理確認消息。

11、如何確保消息接收方消費了消息?

接收方消息確認機制:消費者接收每一條消息後都必須進行確認(消息接收和消息確認是兩個不一樣操做)。只有消費者確認了消息,RabbitMQ才能安全地把消息從隊列中刪除。這裏並無用到超時機制,RabbitMQ僅經過Consumer的鏈接中斷來確認是否須要從新發送消息。也就是說,只要鏈接不中斷,RabbitMQ給了Consumer足夠長的時間來處理消息。

下面羅列幾種特殊狀況:

  • 若是消費者接收到消息,在確認以前斷開了鏈接或取消訂閱,RabbitMQ會認爲消息沒有被分發,而後從新分發給下一個訂閱的消費者。(可能存在消息重複消費的隱患,須要根據bizId去重)
  • 若是消費者接收到消息卻沒有確認消息,鏈接也未斷開,則RabbitMQ認爲該消費者繁忙,將不會給該消費者分發更多的消息。

12、如何避免消息重複投遞或重複消費?
在消息生產時,MQ內部針對每條生產者發送的消息生成一個inner-msg-id,做爲去重和冪等的依據(消息投遞失敗並重傳),避免重複的消息進入隊列;在消息消費時,要求消息體中必需要有一個bizId(對於同一業務全局惟一,如支付ID、訂單ID、帖子ID等)做爲去重和冪等的依據,避免同一條消息被重複消費。

這個問題針對業務場景來答分如下幾點:

  • 好比,你拿到這個消息作數據庫的insert操做。那就容易了,給這個消息作一個惟一主鍵,那麼就算出現重複消費的狀況,就會致使主鍵衝突,避免數據庫出現髒數據。
  • 再好比,你拿到這個消息作redis的set的操做,那就容易了,不用解決,由於你不管set幾回結果都是同樣的,set操做原本就算冪等操做。
  • 若是上面兩種狀況還不行,上大招。準備一個第三方介質,來作消費記錄。以redis爲例,給消息分配一個全局id,只要消費過該消息,將<id,message>以K-V形式寫入redis。那消費者開始消費前,先去redis中查詢有沒消費記錄便可。

十3、如何解決丟數據的問題?
1.生產者丟數據

生產者的消息沒有投遞到MQ中怎麼辦?從生產者弄丟數據這個角度來看,RabbitMQ提供transaction和confirm模式來確保生產者不丟消息。

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

然而缺點就是吞吐量降低了。所以,按照博主的經驗,生產上用confirm模式的居多。一旦channel進入confirm模式,全部在該信道上面發佈的消息都將會被指派一個惟一的ID(從1開始),一旦消息被投遞到全部匹配的隊列以後,rabbitMQ就會發送一個Ack給生產者(包含消息的惟一ID),這就使得生產者知道消息已經正確到達目的隊列了.若是rabiitMQ沒能處理該消息,則會發送一個Nack消息給你,你能夠進行重試操做。

2.消息隊列丟數據

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

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

①、將queue的持久化標識durable設置爲true,則表明是一個持久的隊列

②、發送消息的時候將deliveryMode=2

這樣設置之後,rabbitMQ就算掛了,重啓後也能恢復數據。在消息尚未持久化到硬盤時,可能服務已經死掉,這種狀況能夠經過引入mirrored-queue即鏡像隊列,但也不能保證消息百分百不丟失(整個集羣都掛掉)

3.消費者丟數據

啓用手動確認模式能夠解決這個問題

①自動確認模式,消費者掛掉,待ack的消息迴歸到隊列中。消費者拋出異常,消息會不斷的被重發,直處處理成功。不會丟失消息,即使服務掛掉,沒有處理完成的消息會重回隊列,可是異常會讓消息不斷重試。

②手動確認模式,若是消費者來不及處理就死掉時,沒有響應ack時會重複發送一條信息給其餘消費者;若是監聽程序處理異常了,且未對異常進行捕獲,會一直重複接收消息,而後一直拋異常;若是對異常進行了捕獲,可是沒有在finally裏ack,也會一直重複發送消息(重試機制)。

③不確認模式,acknowledge="none" 不使用確認機制,只要消息發送完成會當即在隊列移除,不管客戶端異常仍是斷開,只要發送完就移除,不會重發。

十4、死信隊列和延遲隊列的使用

死信消息:

消息被拒絕(Basic.Reject或Basic.Nack)而且設置 requeue 參數的值爲 false
消息過時了
隊列達到最大的長度
過時消息:
    在 rabbitmq 中存在2種方可設置消息的過時時間,第一種經過對隊列進行設置,這種設置後,該隊列中全部的消息都存在相同的過時時間,第二種經過對消息自己進行設置,那麼每條消息的過時時間都不同。若是同時使用這2種方法,那麼以過時時間小的那個數值爲準。當消息達到過時時間尚未被消費,那麼那個消息就成爲了一個 死信 消息。
    隊列設置:在隊列申明的時候使用 x-message-ttl 參數,單位爲 毫秒
    單個消息設置:是設置消息屬性的 expiration 參數的值,單位爲 毫秒

延時隊列:在rabbitmq中不存在延時隊列,可是咱們能夠經過設置消息的過時時間和死信隊列來模擬出延時隊列。消費者監聽死信交換器綁定的隊列,而不要監聽消息發送的隊列。

有了以上的基礎知識,咱們完成如下需求:

需求:用戶在系統中建立一個訂單,若是超過期間用戶沒有進行支付,那麼自動取消訂單。

分析:
        一、上面這個狀況,咱們就適合使用延時隊列來實現,那麼延時隊列如何建立
        二、延時隊列能夠由 過時消息+死信隊列 來時間
        三、過時消息經過隊列中設置 x-message-ttl 參數實現
        四、死信隊列經過在隊列申明時,給隊列設置 x-dead-letter-exchange 參數,而後另外申明一個隊列綁定x-dead-letter-exchange對應的交換器。

ConnectionFactory factory = new ConnectionFactory(); 

factory.setHost("127.0.0.1"); 
factory.setPort(AMQP.PROTOCOL.PORT); 
factory.setUsername("guest"); 
factory.setPassword("guest"); 
Connection connection = factory.newConnection(); 
Channel channel = connection.createChannel();

// 聲明一個接收被刪除的消息的交換機和隊列 
String EXCHANGE_DEAD_NAME = "exchange.dead"; 
String QUEUE_DEAD_NAME = "queue_dead"; 
channel.exchangeDeclare(EXCHANGE_DEAD_NAME, BuiltinExchangeType.DIRECT); 
channel.queueDeclare(QUEUE_DEAD_NAME, false, false, false, null); 
channel.queueBind(QUEUE_DEAD_NAME, EXCHANGE_DEAD_NAME, "routingkey.dead"); 

String EXCHANGE_NAME = "exchange.fanout"; 
String QUEUE_NAME = "queue_name"; 
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT); 
Map<String, Object> arguments = new HashMap<String, Object>(); 
// 統一設置隊列中的全部消息的過時時間 
arguments.put("x-message-ttl", 30000); 
// 設置超過多少毫秒沒有消費者來訪問隊列,就刪除隊列的時間 
arguments.put("x-expires", 20000); 
// 設置隊列的最新的N條消息,若是超過N條,前面的消息將從隊列中移除掉 
arguments.put("x-max-length", 4); 
// 設置隊列的內容的最大空間,超過該閾值就刪除以前的消息
arguments.put("x-max-length-bytes", 1024); 
// 將刪除的消息推送到指定的交換機,通常x-dead-letter-exchange和x-dead-letter-routing-key須要同時設置
arguments.put("x-dead-letter-exchange", "exchange.dead"); 
// 將刪除的消息推送到指定的交換機對應的路由鍵 
arguments.put("x-dead-letter-routing-key", "routingkey.dead"); 
// 設置消息的優先級,優先級大的優先被消費 
arguments.put("x-max-priority", 10); 
channel.queueDeclare(QUEUE_NAME, false, false, false, arguments); 
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ""); 
String message = "Hello RabbitMQ: "; 

for(int i = 1; i <= 5; i++) { 
    // expiration: 設置單條消息的過時時間 
    AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties().builder()
            .priority(i).expiration( i * 1000 + ""); 
    channel.basicPublish(EXCHANGE_NAME, "", properties.build(), (message + i).getBytes("UTF-8")); 
} 
channel.close(); 
connection.close();

 
十5、使用了消息隊列會有什麼缺點?
1.系統可用性下降:你想啊,原本其餘系統只要運行好好的,那你的系統就是正常的。如今你非要加個消息隊列進去,那消息隊列掛了,你的系統不是呵呵了。所以,系統可用性下降

2.系統複雜性增長:要多考慮不少方面的問題,好比一致性問題、如何保證消息不被重複消費,如何保證保證消息可靠傳輸。所以,須要考慮的東西更多,系統複雜性增大。

————————————————
版權聲明:本文爲CSDN博主「jeffrey_ding」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連接及本聲明。
原文連接:https://blog.csdn.net/jerryDzan/java/article/details/89183625

相關文章
相關標籤/搜索