RabbitMQ模型架構
生產者和消費者
- Producer:生產者,投遞消息的一方
生產者創建消息,然後發佈到RabbitMQ中。消息包含消息體和標籤。消息體也稱爲payload,實際應用中,消息體一般是一個帶有業務邏輯結構的數據,比如一個JSON字符串。消息的標籤用來表述這條消息,比如一個交換器的名稱和一個路由鍵。生產者把消息交由RabbitMQ,RabbitMQ會根據標籤把消息發送給感興趣的消費者(Consumer) - Consumer:消費者,接收消息的一方
消費者連接到RabbitMQ服務器,並訂閱到隊列上。當消費者消費一條消息時,只是消費消息的消息體(payload)。在消息路由的過程中,消息的標籤會丟棄,存入到隊列中的消息只有消息體,消費者也只會消費到消息體,也不知道消息的生產者是誰 - Broker:消息中間件的服務節點
一個RabbitMQ Broker可以簡單的看作一個RabbitMQ服務節點
隊列
- Queue:隊列,是RabbitMQ的內部對象,用於存儲消息,消息只能存儲在隊列中。
多個消費者可以訂閱同一個隊列,這是隊列中的消息會被平均分攤(Round-Robin)給多個消費者處理。
交換器、路由鍵、綁定
- Exchange:交換器。生產者將消息發送到Exchange,由交換器將消息路由到一個或者多個隊列中。
- RoutingKey:路由鍵。生產者將消息發送給交換器的時候,一般會制定一個RoutingKey,用來指定這個消息的路由規則,而這個RoutingKey需要和交換器類型和綁定鍵(BindingKey)聯合使用才能最終生效
- Binding:綁定。RabbitMQ通過綁定將交換器和隊列關聯起來,在綁定的時候一般會指定一個綁定鍵(BindingKey),這樣RabbitMQ就知道如何正確將消息路由到隊列了。
生產者將消息發送給交換器時,需要一個RoutingKey,當BindingKey和RoutingKey相匹配時,消息會被路由到對應的隊列中。在綁定多個隊列到同一個交換器的時候,這些綁定允許使用相同的BindingKey。BindingKey並不是在所有的情況下都生效,它依賴於交換器類型,比如fanout類型的交換器就會無視Bindingkey,而是將消息路由到所有綁定到該交換器的隊列中。
交換器相當於投遞包裹的郵箱,RoutingKey相當於填寫在包裹上的地址,BindingKey相當於目的地,當填寫在包裹上的地址與實際想要投遞的地址相匹配時,包裹就會被正確的投遞到目的地,最後這個目的地的“主任”—隊列就可以保留這個包裹。如果填寫的地址出錯,郵遞員不能正確投遞到目的地,包裹可能會回退給寄件人,也有可能被丟棄。
channel.echangeDeclare(EXCHANGE_NAME,"direct",true,false,null);
channel.queueDeclare(QUEUE_NAME,true,false,false,null);
channel.queueBind(QUEUE_NAME,ECHANGE_NAME,ROUTING_KEY);
String message = "Hello World";
channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
上面代碼聲明瞭一個direct類型的交換器,然後將交換器和隊列綁定起來。在direct交換器類型下,RoutingKey和BindingKey需要完全匹配才能使用。
在topic交換器類型下,RoutingKey和BindingKey之間需要做模糊匹配,兩者並不是相同的。
BindingKey其實也屬於路由鍵的一種,官方解釋爲:the routing key to use for the binding。可以翻譯爲:在使用的時候使用的路由鍵。
- 在使用綁定的時候,其中需要的路由鍵是BindingKey。涉及的客戶端方法如下:channel.echagneBind、channel.queueBind,對應的AMQP命令爲Exchange.Bind、Queue.Bind
- 在發送消息的時候,其中需要的路由鍵是RoutingKey。涉及的客戶端方法channel.basicPublish,對應的AMQP命令爲Basic.Publish
交換器類型
RabbitMQ常用的交換器類型有fanout、direct、topic、headers四種。AMQP協議裏還有另外兩種類型:System和自定義。
- fanout
它會把所有發送到該交換器的消息路由到所有與該交換器綁定的隊列中 - direct
把消息路由到哪些BindingKey和RoutingKey完全匹配的隊列中
以下圖爲例,發送消息的時候設置路由鍵爲“warning”,則消息會路由到Queue1和Queue2,對應的代碼如下:
channel.basicPublish(EXCHANGE_NAME,“warning”,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes())
如果發送消息的時候設置路由鍵爲“info”或“debug”,消息只會路由到Queue2
- topic
topic類型的交換器在匹配規則上進行了擴展,也是將消息路由到BindingKey和RoutingKey相匹配的隊列中,但這裏的匹配規則有些不同,約定:
- RoutingKey爲一個點號“.”分隔的字符串(被點號“.”分隔開的每一段獨立的字符串稱爲一個單詞),如"com.rabbitmq.client"、“java.util.concurrent”
- BindingKey和RoutingKey一樣也是點號"."分隔的字符串
- BindingKey中可以存在兩種特殊字符串“*”和“#”,用於做模糊匹配,其中“*”用於匹配一個單詞,“#”用於匹配0或多個單詞
上圖的配置:
路由鍵爲“com.rabbitmq.client”的消息會同時路由到Queue1和Queue2;
“com.hidden.client”只會路由到Queue2中;
“com.hidden.demo”只會路由到Queue2中;
“java.rabbitmq.demo”只會路由到Queue1中;
“java.util.concurrent”的消息會被丟棄或者返回給生產者(需要設置mandatory參數),因爲其沒有匹配任何路由鍵
- headers
headers類型的消息不依賴於路由鍵的匹配規則來路由消息,而是根據發送的消息內容中的headers屬性進行匹配。在綁定隊列和交換器時指定一組鍵值對,當發送消息到交換器時,RabbitMQ會獲取該消息的headers(也是一個鍵值對的形式),對比其中的鍵值對是否完全匹配隊列和交換器綁定時指定的鍵值對,如果完全匹配則消息會路由到該隊列,否則不會路由到該隊列。不實用。
RabbitMQ運轉流程
生產者發送消息的流程
- 生產者連接到RabbitMQ Broker,建立一個連接(Connection),開啓一個信道(Channel)
- 生產者聲明一個交換器,並設置相關屬性,比如交換機類型、是否持久化等
- 生產者聲明一個隊列並設置相關屬性,比如是否排他、是否持久化、是否自動刪除等
- 生產者通過路由鍵將交換器和隊列綁定起來
- 生產者發送消息值RabbitMQ Broker,其中包含路由鍵、交換器等信息
- 相應的交換器根據接收到的路由鍵查找相匹配的隊列
- 如果找到,則將生產者發送過來的消息存入相應的隊列中
- 如果沒找到,則根據生產者配置的屬性選擇丟棄還是回退給生產者
- 關閉信道
- 關閉連接
消費者接收消息的過程
- 消費者連接到RabbitMQ Broker,建立一個連接(Connection),開啓一個信道(Channel)
- 消費者向RabbitMQ Broker請求消費相應隊列中的消息,可能會設置相應的回調函數,以及一些準備工作
- 等待RabbitMQ Broker迴應並投遞相應隊列中的消息,接收者接收消息
- 消費者確認(ack)接收到的消息
- RabbitMQ從隊列中刪除相應已經被確認的消息
- 關閉信道
- 關閉連接
無論生產者還是消費者,都需要與RabbitMQ Broker建立連接,這個連接就是一個TCP連接,也就是Connection。一旦TCP連接建立起來,客戶端緊接着就可以創建一個AMQP信道(Channel),每個信道都會被指派唯一的ID。信道是建立在Connection之上的虛擬連接,RabbitMQ處理的每條AMQP指令都是通過信道完成的。
我們完全使用Connection就可以完成信道的工作,爲什麼還要使用信道呢?試想這樣一個場景,一個應用程序中有很多個線程需要從RabbitMQ中消費消息,或者生產消息,那麼必然需要建立多個Connection,也就是多個TCP連接。然而對於操作系統而言,建立和銷燬TCP連接是個非常昂貴的開銷,如果遇到使用高峯,性能瓶頸也隨之顯現。RabbitMQ採用類似NIO的做法,選擇TCP連接複用,不僅可以減少性能開銷,同時也便於管理。
信道在AMQP中是一個很重要的概念,大多數操作都是在信道這個層面展開的。