今天說下了消息隊列中間件,各種隊列性能對比,RabbitMQ隊列,交換機(Exchange)以及消息
中間件的應用場景,然後帶着大家一起實現RabbitMQ的五種消息模型。
消息隊列中間件
消息隊列中間件是分佈式系統中重要的組件,主要解決應用耦合,異步消息,流量削鋒等問題實現高性能,高可用,可伸縮和終一致性[架構] 使用較多的消息隊列有 ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ
消息隊列在實際應用中常用的使用場景:異步處理,應用解耦,流量削鋒和消息通訊四個場景
各種消息中間件性能的比較:
TPS比較 一ZeroMq 最好,RabbitMq 次之, ActiveMq 最差。
持久化消息比較—zeroMq不支持,activeMq和rabbitMq都支持。持久化消息主要是指:MQ down或者MQ所在的服務器down了,消息不會丟失的機制。
可靠性、靈活的路由、集羣、事務、高可用的隊列、消息排序、問題追蹤、可視化管理工具、插件系統、社區—RabbitMq最好,ActiveMq次之,ZeroMq最差。
高併發—從實現語言來看,RabbitMQ最高,原因是它的實現語言是天生具備高併發高可用的erlang語言。
綜上所述:RabbitMQ的性能相對來說更好更全面,是消息中間件的首選。
RabbitMQ理論
RabbitMQ是一個消息隊列,主要作用是用來實現應用程序的解耦和異步(削峯),同時也能起到消息緩存,信息分發的作用。
RabbitMQ由 Erlang 語言開發的 AMQP 的開源實現。
AMQP :Advanced Message Queue,高級消息隊列協議。它是應用層協議的一個開放標準,爲面向消息的中間件設計,基於此協議的客戶端與消息中間件可傳遞消息,並不受產品、開發語言等條件的限制。消息中間件主要用於組件之間的解耦,消息的發送者無需知道消息使用者的存在,反之亦然。AMQP 的主要特徵是面向消息、隊列、路由(包括點對點和發佈/訂閱)、可靠性、安全。
消息中間件最主要的作用是解耦,中間件最標準的用法其實是生產者生產消息發送到隊列,消費者從隊列中拿取消息並處理,生產者不用關係是誰來消費,消費者不用關心誰來生產信息,達到解耦的目的。
在分佈式系統中,消息隊列也會被用在很多其他方面,列如:分佈式事務的支持,RPC的調用等等。其在分佈式系統中存儲轉發消息,在易用性、擴展性、高可用性等方面表現不俗。
特點
RabbitMQ :初起源於金融系統,用於在分佈式系統中存儲轉發消息,在易用性、擴展性、高可用性等方面表現不俗。具體特點包括:
1.可靠性(Reliability)
RabbitMQ 使用一些機制來保證可靠性,如持久化、傳輸確認、發佈確認。
2.靈活的路由(Flexible Routing)
在消息進入隊列之前,通過 Exchange 來路由消息的。對於典型的路由功能,RabbitMQ 已經提供了一些內置的 Exchange 來實現。針對更復雜的路由功能,可以將多個 Exchange 綁定在一起,也通過插件機制實現自己的 Exchange 。
3.消息集羣(Clustering)
多個 RabbitMQ 服務器可以組成一個集羣,形成一個邏輯 Broker 。
4.高可用(Highly Available Queues)
隊列可以在集羣中的機器上進行鏡像,使得在部分節點出問題的情況下隊列仍然可用。
5.多種協議(Multi-protocol)
RabbitMQ 支持多種消息隊列協議,比如 STOMP、MQTT 等等。
6.多語言客戶端(Many Clients)
RabbitMQ 幾乎支持所有常用語言,比如 Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP 等,支持 AJAX。
7.管理界面(Management UI)
RabbitMQ 提供了一個易用的用戶界面,使得用戶可以監控和管理消息 Broker 的許多方 面。
8.跟蹤機制(Tracing)
如果消息異常,RabbitMQ 提供了消息跟蹤機制,使用者可以找出發生了什麼。
9.插件機制(Plugin System)
RabbitMQ 提供了許多插件,來從多方面進行擴展,也可以編寫自己的插件。
相關概念
通常我們談到隊列服務, 會有三個概念: 發消息者、隊列、收消息者,RabbitMQ 在這個基本概念之上, 多做了一層抽象, 在發消息者和 隊列之間, 加入了交換器 (Exchange). 這樣發消息者和隊列就沒有直接聯繫, 轉而變成發消息者把消息給交換器, 交換器根據調度策略再把消息再給隊列。
左側代表生產者,也就是往 RabbitMQ 發消息的程序。
中間即是RabbitMQ,其中包括了 交換機 和 隊列。
右側代表消費者,也就是往 RabbitMQ 拿消息的程序。
其中比較重要的概念有 4 個,分別爲:虛擬主機,交換機,隊列和綁定。
虛擬主機:一個虛擬主機持有一組交換機,隊列和綁定。爲什麼需要多個虛擬主機呢?很簡單, RabbitMQ 當中,用戶只能在虛擬主機的粒度進行權限控制。 因此,如果需要禁止A組訪問B組的交換機/隊列/綁定,必須爲A和B分別創建一個虛擬主機。每一個 RabbitMQ 服務器都有一個默認的虛擬主機。
交換機:Exchange用於轉發信息,但是它不會做存儲,如果每天Queue bind到Exchange的話,它會被直接丟棄掉Producer發送過來的信息。
這裏比較重要的一個概念:路由鍵。消息到交換機的時候,交換機會轉發到對應的隊列中,那麼究竟轉發到哪個隊列,就根據該路由鍵判斷。
隊列的作用在上面已經說過這裏就不在說明。
綁定:是交換機需要和隊列相綁定,這其中如上圖所示,就是多對多的關係。
圖中所示概念:
RabbitMQ Server:也叫broker server,它是一種傳輸服務。 他的角色就是維護一條 從Producer到Consumer的路線,保證數據能夠按照指定的方式進行傳輸。
Producer: 消息生產者,如圖A、B、C,數據的發送方。消息生產者連接RabbitMQ服 務器然後將消息投遞到Exchange。
Consumer:消息消費者,如圖1、2、3,數據的接收方。消息消費者訂閱隊列, RabbitMQ將Queue中的消息發送到消息消費者。
Exchange:生產者將消息發送到Exchange(交換器),由Exchange將消息路由到一個或多個Queue中(或者丟棄)。Exchange並不存儲消息。RabbitMQ中的Exchange有 direct、fanout、topic、headers四種類型,每種類型對應不同的路由規則。
Queue:(隊列)是RabbitMQ的內部對象,用於存儲消息。消息消費者就是通過訂閱 隊列來獲取消息的,RabbitMQ中的消息都只能存儲在Queue中,生產者生產消息並終 投遞到Queue中,消費者可以從Queue中獲取消息並消費。多個消費者可以訂閱同一個 Queue,這時Queue中的消息會被平均分攤給多個消費者進行處理,而不是每個消費者 都收到所有的消息並處理。
RoutingKey:生產者在將消息發送給Exchange的時候,一般會指定一個routing key, 來指定這個消息的路由規則,而這個routing key需要與Exchange Type及binding key聯 合使用才能終生效。在Exchange Type與binding key固定的情況下(在正常使用時一 般這些內容都是固定配置好的),我們的生產者就可以在發送消息給Exchange時,通過 指定routing key來決定消息流向哪裏。RabbitMQ爲routing key設定的長度限制爲255 bytes。
交換機(Exchange)詳解:
交換機的功能主要是接收消息並且轉發到綁定的隊列,交換機不存儲消息,在啓用ack模式後,交換機找不到隊列會返回錯誤。交換機有四種類型:Direct, topic, Headers and Fanout
Direct:direct類型的行爲是"先匹配,再投送",即在綁定時設定一個routing_key,消息的routing_key匹配時,纔會被交換機投送到綁定的隊列中去。
Topic:按照規則轉發信息(最靈活)
Headers:設置header attribute參數類型的交換機
Fanout:轉發信息到所有綁定隊列
Direct Exchange
Direct Exchange 是 RabbitMQ 默認的交換機模式,也是最簡單的模式,根據key全文匹配去尋找隊列。
第一個 X - Q1 就有一個 binding key,名字爲 orange; X - Q2 就有 2 個 binding key,名字爲 black 和 green。當消息中的 路由鍵 和 這個 binding key 對應上的時候,那麼就知道了該消息去到哪一個隊列中。
Ps:爲什麼 X 到 Q2 要有 black,green,2個 binding key呢,一個不就行了嗎? - 這個主要是因爲可能又有 Q3,而Q3只接受 black 的信息,而Q2不僅接受black 的信息,還接受 green 的信息。
Topic Exchange
Topic Exchange 轉發消息主要是根據通配符。 在這種交換機下,隊列和交換機的綁定會定義一種路由模式,那麼,通配符就要在這種路由模式和路由鍵之間匹配後交換機才能轉發消息。
在這種交換機模式下:
路由鍵必須是一串字符,用句號(.) 隔開,比如說 agreements.us,或者 agreements.eu.stockholm 等。
路由模式必須包含一個 星號(),主要用於匹配路由鍵指定位置的一個單詞,比如說,一個路由模式是這樣子:agreements…b.,那麼就只能匹配路由鍵是這樣子的:第一個單詞是 agreements,第四個單詞是 b。 井號(#)就表示相當於一個或者多個單詞,例如一個匹配模式是agreements.eu.berlin.#,那麼,以agreements.eu.berlin 開頭的路由鍵都是可以的。
具體代碼發送的情形如下,第一個參數表示交換機,第二個參數表示 routing key,第三個參數即消息。如下:
rabbitTemplate.convertAndSend("testTopicExchange","key1.a.c.key2", " this is RabbitMQ!");
topic 和 direct 類似, 只是匹配上支持了"模式", 在"點分"的 routing_key 形式中, 可以使用兩個通配符:
*:表示一個詞.
#:表示零個或多個詞.
Headers Exchange
headers 也是根據規則匹配, 相較於 direct 和 topic 固定地使用 routing_key , headers 則是一個自定義匹配規則的類型.
在隊列與交換器綁定時, 會設定一組鍵值對規則, 消息中也包括一組鍵值對( headers 屬性), 當這些鍵值對有一對, 或全部匹配時, 消息被投送到對應隊列.
Fanout Exchange
Fanout Exchange 消息廣播的模式,不管路由鍵或者是路由模式,會把消息發給綁定給它的全部隊列,如果配置了 routing_key 會被忽略。
最後講解一下消息中間件的應用場景就開始整合。
消息中間件的應用場景
異步處理:
場景說明:用戶註冊後,需要發註冊郵件和註冊短信,傳統的做法有兩種1.串行的方式;2.並行的方式
(1)串行方式:將註冊信息寫入數據庫後,發送註冊郵件,再發送註冊短信,以上三個任務全部完成後才返回給客戶端。 這有一個問題是,郵件,短信並不是必須的,它只是一個通知,而這種做法讓客戶端等待沒有必要等待的東西.
(2)並行方式:將註冊信息寫入數據庫後,發送郵件的同時,發送短信,以上三個任務完成後,返回給客戶端,並行的方式能提高處理的時間。
假設三個業務節點分別使用50ms,串行方式使用時間150ms,並行使用時間100ms。雖然並性已經提高的處理時間,但是,前面說過,郵件和短信對我正常的使用網站沒有任何影響,客戶端沒有必要等着其發送完成才顯示註冊成功,應該是寫入數據庫後就返回.
(3)消息隊列
引入消息隊列後,把發送郵件,短信不是必須的業務邏輯異步處理
由此可以看出,引入消息隊列後,用戶的響應時間就等於寫入數據庫的時間+寫入消息隊列的時間(可以忽略不計),引入消息隊列後處理後,響應時間是串行的3倍,是並行的2倍。
應用解耦
場景:雙11是購物狂節,用戶下單後,訂單系統需要通知庫存系統,傳統的做法就是訂單系統調用庫存系統的接口.
這種做法有一個缺點:
當庫存系統出現故障時,訂單就會失敗。訂單系統和庫存系統高耦合. 引入消息隊列
訂單系統:用戶下單後,訂單系統完成持久化處理,將消息寫入消息隊列,返回用戶訂單下單成功。
庫存系統:訂閱下單的消息,獲取下單消息,進行庫操作。
就算庫存系統出現故障,消息隊列也能保證消息的可靠投遞,不會導致消息丟失。
流量削峯
流量削峯一般在秒殺活動中應用廣泛
場景:秒殺活動,一般會因爲流量過大,導致應用掛掉,爲了解決這個問題,一般在應用前端加入消息隊列。
作用:
1.可以控制活動人數,超過此一定閥值的訂單直接丟棄(我爲什麼秒殺一次都沒有成功過呢^^)
2.可以緩解短時間的高流量壓垮應用(應用程序按自己的最大處理能力獲取訂單)
1.用戶的請求,服務器收到之後,首先寫入消息隊列,加入消息隊列長度超過最大值,則直接拋棄用戶請求或跳轉到錯誤頁面.
2.秒殺業務根據消息隊列中的請求信息,再做後續處理.
SpringBoot整合RabbitMQ 實現五種消息模型
上面的文章詳細講解了消息隊列中間件,各種隊列性能對比,RabbitMQ隊列,交換機(Exchange)以及消息中間件的應用場景接下來進入整合實現五種消息模型。
RabbitMQ提供了6種消息模型,但是第6種其實是RPC,並不是MQ,因此不予學習。那麼也就剩下5種。
基本消息模型:生產者–>隊列–>消費者
work消息模型:生產者–>隊列–>多個消費者共同消費
訂閱模型-Fanout:廣播模式,將消息交給所有綁定到交換機的隊列,每個消費者都會收到同一條消息
訂閱模型-Direct:定向,把消息交給符合指定 rotingKey 的隊列
訂閱模型-Topic 主題模式:通配符,把消息交給符合routing pattern(路由模式) 的隊列
首先打開abbitmq容器,然後安裝rabbitmq可視化插件。
打開容器
docker start CID
執行命令進入安裝好的容器內
docker exec -it rabbitmq /bin/bash
執行命令安裝可視化插件
rabbitmq-plugins enable rabbitmq_management
http://你的ip:15672,用戶名:guest,密碼:guest進入rabbitmq管理界面
基本消息模型(簡單隊列):
P:消息的生產者
C:消息的消費者
紅色:隊列
生產者將消息發送到隊列,消費者從隊列中獲取消息。
1.配置pom文件,主要是添加spring-boot-starter-amqp的支持
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、配置application.properties文件
配置rabbitmq的安裝地址、端口以及賬戶信息
spring:
application:
name: spirng-boot-rabbitmq
rabbitmq:
host: 192.168.72.129
port: 5672
username: admin
password: admin
port:5672 是RabbitMQ默認端口
3、配置隊列
package com.szh.springboot_rabbitmq.config;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitConfig {
@Bean
public Queue queue() {
return new Queue("q_hello");
}
}
4.消息的生產者(發送信息)
package com.szh.springboot_rabbitmq.send;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component
public class HelloSender {
@Autowired
private AmqpTemplate rabbitTemplate;
public void send() {
String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());//24小時制
String context = "hello " + date;
System.out.println("Sender : " + context);
//簡單對列的情況下routingKey即爲Q名
this.rabbitTemplate.convertAndSend("q_hello", context);
}
}
5.消息的消費者(接收)
package com.szh.springboot_rabbitmq.receive;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@RabbitListener(queues = "q_hello")
public class HelloReceiver {
@RabbitHandler
public void process(String hello) {
System.out.println("Receiver : " + hello);
}
6、測試
package com.szh.springboot_rabbitmq;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class RabbitMqHelloTest {
@Autowired
private HelloSender helloSender;
//基本消息模式 直接模式 單對單
@Test
public void hello() throws Exception {
helloSender.send();
}
}
多對多使用(Work模式)
如圖所示:
一個生產者、2個消費者。
一個消息只能被一個消費者獲取。
複用上面單對單的簡單隊列增加一個Receiver
package com.szh.springboot_rabbitmq.receive;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@RabbitListener(queues = "q_hello")
public class HelloReceiver2 {
@RabbitHandler
public void process(String hello) {
System.out.println("Receiver2 : " + hello);
}
}
修改消息的生產者
public void send(int i) {
String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());//24小時制
String context = "hello " + i + " " + date;
System.out.println("Sender : " + context);
//簡單對列的情況下routingKey即爲Q名
this.rabbitTemplate.convertAndSend("q_hello", context);
}
修改測試類
//work工作模式(輪詢發佈,公平發佈)
@Test
public void oneToMany() throws Exception {
for (int i=0;i<100;i++){
helloSender.send(i);
Thread.sleep(300);
}
}
測試結果:
1、消費者1和消費者2獲取到的消息內容是不同的,同一個消息只能被一個消費者獲取。
2、消費者1和消費者2獲取到的消息的數量是相同的,一個是消費奇數號消息,一個是偶數。
其實,這樣是不合理的,因爲消費者1線程停頓的時間短。應該是消費者1要比消費者2獲取到的消息多才對。
RabbitMQ 默認將消息順序發送給下一個消費者,這樣,每個消費者會得到相同數量的消息。即輪詢(round-robin)分發消息。
2個概念
輪詢分發 :使用任務隊列的優點之一就是可以輕易的並行工作。如果我們積壓了好多工作,我們可以通過增加工作者(消費者)來解決這一問題,使得系統的伸縮性更加容易。在默認情況下,RabbitMQ將逐個發送消息到在序列中的下一個消費者(而不考慮每個任務的時長等等,且是提前一次性分配,並非一個一個分配)。平均每個消費者獲得相同數量的消息。這種方式分發消息機制稱爲Round-Robin(輪詢)。
公平分發 :雖然上面的分配法方式也還行,但是有個問題就是:比如:現在有2個消費者,所有的奇數的消息都是繁忙的,而偶數則是輕鬆的。按照輪詢的方式,奇數的任務交給了第一個消費者,所以一直在忙個不停。偶數的任務交給另一個消費者,則立即完成任務,然後閒得不行。而RabbitMQ則是不瞭解這些的。這是因爲當消息進入隊列,RabbitMQ就會分派消息。它不看消費者爲應答的數目,只是盲目的將消息發給輪詢指定的消費者。
公平分發模式在Spring-amqp中是默認的,這種情況也是日常工作中使用最爲正常的,輪詢模式用的較少,區別在於prefetch默認是1,如果設置爲0就是輪詢模式。
公平分發模式,輪詢分發模式 查看該博客 https://blog.csdn.net/saytime/article/details/80541450
訂閱模式
1、1個生產者,多個消費者
2、每一個消費者都有自己的一個隊列
3、生產者沒有將消息直接發送到隊列,而是發送到了交換機
4、每個隊列都要綁定到交換機
5、生產者發送的消息,經過交換機,到達隊列,實現,一個消息被多個消費者獲取的目的
注意:一個消費者隊列可以有多個消費者實例,只有其中一個消費者實例會消費
Topic Exchange(主題模式,通配符模式):
同一個消息被多個消費者獲取。一個消費者隊列可以有多個消費者實例,只有其中一個消費者實例會消費到消息。
topic 是RabbitMQ中最靈活的一種方式,可以根據routing_key自由的綁定不同的隊列
Topic類型的Exchange與Direct相比,都是可以根據RoutingKey把消息路由到不同的隊列。只不過Topic類型Exchange可以讓隊列在綁定Routing key 的時候使用通配符!
Routingkey 一般都是有一個或多個單詞組成,多個單詞之間以”.”分割,例如: user.insert
通配符規則 | 舉例 |
---|---|
#:匹配一個或多個詞 | user.#:能夠匹配user.insert.save 或者 user.insert |
*:匹配不多不少恰好1個詞 | user.*:只能匹配user.insert |
首先對topic規則配置,這裏使用兩個隊列(消費者)來演示。
1.配置隊列,綁定交換機。
package com.szh.springboot_rabbitmq.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TopicRabbitConfig {
//隊列名
final static String message = "q_topic_message";
final static String messages = "q_topic_messages";
//創建隊列
@Bean
public Queue queueMessage() {
return new Queue(TopicRabbitConfig.message);
}
@Bean
public Queue queueMessages() {
return new Queue(TopicRabbitConfig.messages);
}
/**
* 聲明一個Topic類型的交換機
* @return
*/
@Bean
TopicExchange exchange() {
return new TopicExchange("mybootexchange");
}
/**
* 綁定Q隊列到交換機,並且指定routingKey
* @param queueMessage
* @param exchange
* @return
*/
@Bean
Binding bindingExchangeMessage(Queue queueMessage, TopicExchange exchange) {
return BindingBuilder.bind(queueMessage).to(exchange).with("topic.message");
}
@Bean
Binding bindingExchangeMessages(Queue queueMessages, TopicExchange exchange) {
return BindingBuilder.bind(queueMessages).to(exchange).with("topic.#");
}
}
2.創建2個消費者
q_topic_message 和q_topic_messages
package com.szh.springboot_rabbitmq.receive;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@RabbitListener(queues = "q_topic_message")
public class Receiver1 {
@RabbitHandler
public void process(String hello) {
System.out.println("Receiver1 : " + hello);
}
}
package com.szh.springboot_rabbitmq.receive;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@RabbitListener(queues = "q_topic_messages")
public class Receiver2 {
@RabbitHandler
public void process(String hello) {
System.out.println("Receiver2 : " + hello);
}
}
3.消息生產者(發送信息)
package com.szh.springboot_rabbitmq.send;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MsgSender {
@Autowired
private AmqpTemplate rabbitTemplate;
/**
* 這裏的mybootexchange是交換機的名稱字符串和發送消息時的名稱必須相同
* 具體代碼發送的情形如下,第一個參數表示交換機,第二個參數表示 routing key,第三個參數即消息。如下:
* this.rabbitTemplate.convertAndSend("mybootexchange", "topic.message", context);
*/
public void send1() {
String context = "hi, i am message 1";
System.out.println("Sender : " + context);
this.rabbitTemplate.convertAndSend("mybootexchange", "topic.message", context);
}
public void send2() {
String context = "hi, i am messages 2";
System.out.println("Sender : " + context);
this.rabbitTemplate.convertAndSend("mybootexchange", "topic.messages", context);
}
}
send1方法會匹配到topic.#和topic.message,兩個Receiver都可以收到消息,發送send2只有topic.#可以匹配所有隻有Receiver2監聽到消息。
4.測試
package com.szh.springboot_rabbitmq;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class RabbitTopicTest {
@Autowired
private MsgSender msgSender;
/*主題模式
topic 是RabbitMQ中最靈活的一種方式,可以根據routing_key自由的綁定不同的隊列
首先對topic規則配置,這裏使用兩個隊列(消費者)來演示。
通配符,把消息交給符合routing pattern(路由模式) 的隊列
*/
@Test
public void send1() throws Exception {
msgSender.send1();
}
@Test
public void send2() throws Exception {
msgSender.send2();
}
}
Fanout Exchange(訂閱模式-廣播)
Fanout,也稱爲廣播。在廣播模式下,消息發送流程是這樣的:
1.可以有多個消費者
2.每個消費者有自己的queue(隊列)
3.每個隊列都要綁定到Exchange(交換機)
4.生產者發送的消息,只能發送到交換機,交換機來決定要發給哪個隊列,生產者無法決定。
5.交換機把消息發送給綁定過的所有隊列
6.隊列的消費者都能拿到消息。實現一條消息被多個消費者消費
Fanout 就是我們熟悉的廣播模式或者訂閱模式,給Fanout交換機發送消息,綁定了這個交換機的所有隊列都收到這個消息。
1.配置隊列,綁定交換機
package com.szh.springboot_rabbitmq.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FanoutRabbitConfig {
/**
* @author songzhenghong
* @version 1.0
* @date 2019/6/16 10:52
* Broker:它提供一種傳輸服務,它的角色就是維護一條從生產者到消費者的路線,保證數據能按照指定的方式進行傳輸,
* Exchange:消息交換機,它指定消息按什麼規則,路由到哪個隊列。
* Queue:消息的載體,每個消息都會被投到一個或多個隊列。
* Binding:綁定,它的作用就是把exchange和queue按照路由規則綁定起來.
* Routing Key:路由關鍵字,exchange根據這個關鍵字進行消息投遞。
* vhost:虛擬主機,一個broker裏可以有多個vhost,用作不同用戶的權限分離。
* Producer:消息生產者,就是投遞消息的程序.
* Consumer:消息消費者,就是接受消息的程序.
* Channel:消息通道,在客戶端的每個連接裏,可建立多個channel.
*/
//創建隊列
@Bean
public Queue aMessage() {
return new Queue("q_fanout_A");
}
@Bean
public Queue bMessage() {
return new Queue("q_fanout_B");
}
@Bean
public Queue cMessage() {
return new Queue("q_fanout_C");
}
/**
* 聲明一個Fanout類型的交換機
* @return
*/
/**
* 針對消費者配置
* 1. 設置交換機類型
* 2. 將隊列綁定到交換機
FanoutExchange: 將消息分發到所有的綁定隊列,無routingkey的概念
DirectExchange:按照routingkey分發到指定隊列
TopicExchange:多關鍵字匹配
*/
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange("mybootfanoutExchange");
}
/**
* 綁定aMessage隊列到交換機
* @param aMessage
* @param fanoutExchange
* @return
*/
@Bean
Binding bindingExchangeA(Queue aMessage, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(aMessage).to(fanoutExchange);
}
@Bean
Binding bindingExchangeB(Queue bMessage, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(bMessage).to(fanoutExchange);
}
@Bean
Binding bindingExchangeC(Queue cMessage, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(cMessage).to(fanoutExchange);
}
}
2.創建3個消費者
package com.szh.springboot_rabbitmq.receive;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@RabbitListener(queues = "q_fanout_A")
public class ReceiverA {
@RabbitHandler
public void process(String hello) {
System.out.println("AReceiver : " + hello + "/n");
}
}
package com.szh.springboot_rabbitmq.receive;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@RabbitListener(queues = "q_fanout_B")
public class ReceiverB {
@RabbitHandler
public void process(String hello) {
System.out.println("BReceiver : " + hello + "/n");
}
}
package com.szh.springboot_rabbitmq.receive;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@RabbitListener(queues = "q_fanout_C")
public class ReceiverC {
@RabbitHandler
public void process(String hello) {
System.out.println("CReceiver : " + hello + "/n");
}
}
3.生產者
package com.szh.springboot_rabbitmq.send;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MsgSenderFanout {
@Autowired
private AmqpTemplate rabbitTemplate;
public void send() {
String context = "hi, fanout msg ";
System.out.println("Sender : " + context);
this.rabbitTemplate.convertAndSend("mybootfanoutExchange","", context);
}
}
4.測試
package com.szh.springboot_rabbitmq;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class RabbitFanoutTest {
@Autowired
private MsgSenderFanout msgSender;
@Test
public void send1() throws Exception {
msgSender.send();
}
}
結果如下,三個消費者都收到消息:
AReceiver : hi, fanout msg
CReceiver : hi, fanout msg
BReceiver : hi, fanout msg
訂閱模型-Direct
在Fanout模式中,一條消息,會被所有訂閱的隊列都消費。但是,在某些場景下,我們希望不同的消息被不同的隊列消費。這時就要用到Direct類型的Exchange。
在Direct模型下:
隊列與交換機的綁定,不能是任意綁定了,而是要指定一個RoutingKey(路由key)
消息的發送方在 向 Exchange發送消息時,也必須指定消息的 RoutingKey。
Exchange不再把消息交給每一個綁定的隊列,而是根據消息的Routing Key進行判斷,只有隊列的Routingkey與消息的 Routing key完全一致,纔會接收到消息.
1.配置隊列, 綁定交換機
1.生產者(發送消息)
package com.szh.springboot_rabbitmq.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DirectRabbitConfig {
@Bean
public Queue q_direct_A() {
return new Queue("q_direct_A");
}
@Bean
public Queue q_direct_B() {
return new Queue("q_direct_B");
}
@Bean
public Queue q_direct_C() {
return new Queue("q_direct_C");
}
@Bean
DirectExchange directExchange() {
return new DirectExchange("mybootdirectExchange");
}
@Bean
Binding bindingExchangeDA(Queue q_direct_A, DirectExchange directExchange) {
return BindingBuilder.bind(q_direct_A).to(directExchange).with("topic");
}
@Bean
Binding bindingExchangeDB(Queue q_direct_B, DirectExchange directExchange) {
return BindingBuilder.bind(q_direct_B).to(directExchange).with("topic");
}
@Bean
Binding bindingExchangeDC(Queue q_direct_C, DirectExchange directExchange) {
return BindingBuilder.bind(q_direct_C).to(directExchange).with("topic");
}
}
仍是之前Fanout創建好的三個消費者
2.生產者
package com.szh.springboot_rabbitmq.send;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MsgProducers {
@Autowired
private AmqpTemplate rabbitTemplate;
public void producerA(){
String context = "hi, direct msg ";
System.out.println("Sender : " + context);
this.rabbitTemplate.convertAndSend("mybootdirectExchange","topic", context);
}
}
3.測試
@Test
public void send1() throws Exception {
msgProducers.producerA();
}
結果如下,三個消費者都收到消息:
AReceiver : hi, direct msg
CReceiver : hi, direct msg
BReceiver : hi, direct msg
這是因爲3個隊列綁定交換機的rountingkey都是topic所導致的。