rabbitmq常見面試題

1. 如何確保消息正確地發送至RabbitMQ?

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

 

2. 如何確保消息接收方消費了消息?

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

下面羅列幾種特殊情況:

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

3. 如何避免消息重複投遞或重複消費?

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

4. 消息基於什麼傳輸?

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

5. 消息如何分發?

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

6. 消息怎麼路由?

從概念上來說,消息路由必須有三部分:交換器、路由、綁定。生產者把消息發佈到交換器上;綁定決定了消息如何從路由器路由到特定的隊列;消息最終到達隊列,並被消費者接收。

  1. 消息發佈到交換器時,消息將擁有一個路由鍵(routing key),在消息創建時設定。
  2. 通過隊列路由鍵,可以把隊列綁定到交換器上。
  3. 消息到達交換器後,RabbitMQ會將消息的路由鍵與隊列的路由鍵進行匹配(針對不同的交換器有不同的路由規則)。如果能夠匹配到隊列,則消息會投遞到相應隊列中;如果不能匹配到任何隊列,消息將進入 “黑洞”。

常用的交換器主要分爲一下三種:

  • direct:如果路由鍵完全匹配,消息就被投遞到相應的隊列
  • fanout:如果交換器收到消息,將會廣播到所有綁定的隊列上
  • topic:可以使來自不同源頭的消息能夠到達同一個隊列。 使用topic交換器時,可以使用通配符,比如:“*” 匹配特定位置的任意文本, “.” 把路由鍵分爲了幾部分,“#” 匹配所有規則等。特別注意:發往topic交換器的消息不能隨意的設置選擇鍵(routing_key),必須是由"."隔開的一系列的標識符組成。

7. 如何確保消息不丟失?

消息持久化的前提是:將交換器/隊列的durable屬性設置爲true,表示交換器/隊列是持久交換器/隊列,在服務器崩潰或重啓之後不需要重新創建交換器/隊列(交換器/隊列會自動創建)。如果消息想要從Rabbit崩潰中恢復,那麼消息必須:

  • 在消息發佈前,通過把它的 “投遞模式” 選項設置爲2(持久)來把消息標記成持久化
  • 將消息發送到持久交換器
  • 消息到達持久隊列

RabbitMQ確保持久性消息能從服務器重啓中恢復的方式是,將它們寫入磁盤上的一個持久化日誌文件,當發佈一條持久性消息到持久交換器上時,Rabbit會在消息提交到日誌文件後才發送響應(如果消息路由到了非持久隊列,它會自動從持久化日誌中移除)。一旦消費者從持久隊列中消費了一條持久化消息,RabbitMQ會在持久化日誌中把這條消息標記爲等待垃圾收集。如果持久化消息在被消費之前RabbitMQ重啓,那麼Rabbit會自動重建交換器和隊列(以及綁定),並重播持久化日誌文件中的消息到合適的隊列或者交換器上。

8. 使用RabbitMQ有什麼好處?

  • 應用解耦(系統拆分)
  • 異步處理(預約掛號業務處理成功後,異步發送短信、推送消息、日誌記錄等)
  • 消息分發
  • 流量削峯
  • 消息緩衝

9、RabbitMQ 中的 broker 是指什麼?cluster 又是指什麼?

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

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

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

11:RabbitMQ 概念裏的 channel、exchange 和 queue 是邏輯概念,還是對應着進程實體?分別起什麼作用?

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

12、vhost 是什麼?起什麼作用?

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


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

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

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

14、死信隊列和延遲隊列的使用

死信消息:

  1. 消息被拒絕(Basic.Reject或Basic.Nack)並且設置 requeue 參數的值爲 false
  2. 消息過期了
  3. 隊列達到最大的長度

過期消息:

    在 rabbitmq 中存在2種方可設置消息的過期時間,第一種通過對隊列進行設置,這種設置後,該隊列中所有的消息都存在相同的過期時間,第二種通過對消息本身進行設置,那麼每條消息的過期時間都不一樣。如果同時使用這2種方法,那麼以過期時間小的那個數值爲準。當消息達到過期時間還沒有被消費,那麼那個消息就成爲了一個 死信 消息。

    隊列設置:在隊列申明的時候使用 x-message-ttl 參數,單位爲 毫秒

    單個消息設置:是設置消息屬性的 expiration 參數的值,單位爲 毫秒

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

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

需求:用戶在系統中創建一個訂單,如果超過時間用戶沒有進行支付,那麼自動取消訂單。

分析:

        1、上面這個情況,我們就適合使用延時隊列來實現,那麼延時隊列如何創建

        2、延時隊列可以由 過期消息+死信隊列 來時間

        3、過期消息通過隊列中設置 x-message-ttl 參數實現

        4、死信隊列通過在隊列申明時,給隊列設置 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();

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章