概述:
1. 大多應用中,可通過消息服務中間件來提升系統異步通信、擴展解耦能力。
以用戶註冊創建賬戶爲例:
註冊信息寫入數據庫 ,發送註冊郵件,發送註冊信息
假定該三步操作都需要50ms
如上圖所示, 放在以前,當我們進行同步調用時,寫入數據庫後,調用發送註冊郵件的方法,在調用發送註冊短信的, 如第一種所示,則需要150ms ,用戶收到註冊成功回饋。
當我們採用多線程的方式,發送註冊郵件和發送註冊短信異步執行,仍舊需要 100ms,用戶收到註冊成功回饋。如第二種所示。
此時,如果我們引入消息隊列,當註冊信息寫入數據庫後,可以很快的將該信息寫入隊列中(時間很短),此時用戶就可以收到響應註冊成功的消息。而發送郵件和發送短信的業務,都可以通過異步自動讀取這些信息,給用戶發郵件、發消息。如第三種所示。
再如: 應用解耦
比如有訂單系統和庫存 系統,假如寫在一個應用中,如第一種,下完單後,再去調用庫存系統,其耦合度很高,同時也像第一個案例一樣,響應時間過長。
如果把庫存和訂單系統單獨抽離,例如微服務形式調用,如果在引入 消息隊列,下單後,將訂單信息寫入消息隊列,而庫存系統通過訂閱消息隊列中的消息,來進行庫存的相關業務操作,這就大大降低了耦合度,而且提高了響應時間。
再如:流量削峯,特別是用於秒殺系統
假如某件商品僅有1萬份,十萬人進行秒殺,如果每一個人的請求都發過來,tomcat進行處理,這將瞬間擊垮服務器,此時怎麼做呢? 我們可以把用戶的請求放入到消息隊列中,並給消息隊列指定長度,比如1萬,此時用戶的速度快,就搶先進入隊列,當消息隊列長度(用戶請求書)達到上限(1萬),就立馬拋出響應失敗,也即其他9萬人的請求,立馬被拋回。 而這些在消息隊列中的內容(用戶請求),秒殺業務就開始慢慢取出內容,進行相關業務操作。
通過這三個案例,就展示了爲什麼要引入消息中間件---消息隊列。
2. 消息服務的兩個重要概念:
消息代理(message borker) 和 目的地(destination),當消息發送者發送消息後,將有消息代理接管,消息代理保證消息傳遞到指定目的地。
3. 主要有兩種形式的目的地:
1. 隊列(queue): 點對點消息通信(point-to-point)
2. 主題(topic): 發佈(publish) /訂閱(subscribe) 消息通信
4. 點對點式:
- 消息發送者發送消息,消息代理將其放入一個隊列中,消息接受者從隊列中獲取消息內容,消息讀取後被移除隊列
- 消息只有唯一的發送者和接受者,但不是說只能有一個接收者。
5. 發佈訂閱式:
- 發送者(發佈者)發送消息到主題,多個接收者(訂閱者)監聽(訂閱)這個主題,那麼就會在消息到達時同時接收到消息
6. JMS (Java Message Service)Java消息服務
- 基於JVM消息代理的規範。ActiveMQ、HornetMQ是JMS實現。
7. AMQP(Advanced Message Queuing Protocol)
-高級消息隊列協議,也是一個消息代理的規範,兼容JMS
- RabbitMQ是AMQP的實現。
JMS和AMQP的對比如下:
8. Spring支持
- Spring-jms 提供了對JMS的支持
- spring-rabbit 提供了對AMQP的支持
- 需要ConnectionFactory的實現來連接消息代理
- 提供JmsTemplate、RabbitTemplate來發送消息
- @JmsListener(JMS) 、@RabbitListener(AMQP) 註解在方法上監聽消息代理髮布的消息
- @EnableJms、@EnableRabbit 開啓支持
9. Spring Boot自動配置
- JmsAutoConfiguration
- RabbitAutoConfiguration
下面介紹 RabbitMQ相關內容:
RabbitMQ簡介
是一個由erlang開發的AMQP(Advanved Message Queue Protocol)的開源實現。
核心概念
Message
消息,是不具名的,由消息頭和消息體組成。消息體是不透明的,而消息頭則由一系列的可選屬性組成,這些屬性包括routing-key(路由鍵)、priority(相對於其他消息的優先權)、delivery-mode(指出該消息可能需要持久性存儲)等。
Publisher
消息的生產者,也是一個向交換器發佈消息的客戶端應用程序。
Exchange
交換器,用來接收生產者發送的消息,並將這些消息路由給服務器中的隊列。有4中類型:direct(默認),fanout,topic,和headers,不同類型的Exchange轉發消息的策略有所區別。
Queue
消息隊列,用來保存消息直到發送到消費者。它是消息的容器,也是消息的終點,一個消息可投入一個或多個隊列。消息一直在隊列裏面,等待消費者連接到這個隊列將其取走。
Binding
綁定,用於消息隊列和交換器之間的關聯。一個綁定就是基於路由鍵將交換器和消息隊列連接起來的路由規則,所以可以將交換器理解爲一個由綁定構成的路由表。
Exchange和Queue的綁定可以是多對多的關係。
Connection
網絡連接,比如一個TCP連接。
Channel
信道,多路複用連接中的一條獨立的雙向數據流通道。信道是建立在真實的tcp連接內的虛擬連接,AMQP命令都是通過信道發出去的,不管是發佈消息、訂閱隊列還是接受消息,這些動作都是通過信道完成。因爲對於操作系統來說,建立和銷燬tcp都是非常昂貴的開銷,所以引入信道的概念,以複用一條tcp連接。
Consumer
消息的消費者,表示一個從消息隊列取得消息的客戶端應用程序。
Virtual Host
虛擬主機,表示一批交換機、消息隊列和相關對象。虛擬主機是共享共同的身份認證和加密環境的獨立服務器域。每個vhost本質上就是一個mini版的RabbitMQ服務器,擁有自己的隊列、交換器、綁定和權限機制,vhost是AMQP概念的基礎,必須在連接時指定,RabbitMQ默認的vhost是 /
Broker
表示消息隊列服務器實體
2. RabbitMQ運行機制
AMQP中的消息路由
AMQP中消息的路由過程和Java開發者熟悉的JMS存在一定差別,AMQP中增加了Exchange和Binding的角色。生產者把消息發佈到Exchange上,消息最終到達隊列並被消費者接收,而Binding決定交換器的消息應該發送到哪個隊列。
Exchange類型
Exchange分發消息時根據類型的不同分發策略有區別,目前共四種類型:direct,fanout,topic,headers。headers匹配AMQP消息的header而不是路由鍵,headers交換器和direct交換器完全一致,但性能差很多,目前幾乎不用,所以直接瞭解其它三種類型:
Direct Exchange :消息中的路由鍵(routing key) ,如果和Binding中的binding key一致,交換器就將消息發到對應的隊列中。路由鍵與隊列名完全匹配,如果一個隊列綁定到交換機要求路由鍵爲"dog",則只會轉發 routing key 標記的爲 "dog"的消息,不會轉發"dog.puppy" ,也不會轉發"dog.guard"等等。它是完全匹配、單播的模式。
fanout Exchange : 每個發送fanout類型轉換器的消息都會分到所有綁定的隊列上去。fanout交換器不處理路由鍵,只是簡單的將隊列綁定到交換器上,每個發送到交換器的消息都會被轉發到與該交換器綁定的所有隊列上。很像子網廣播,每臺子網內的主機都獲得了一份複製的消息。fanout類型轉發消息是最快的。
topic Exchange: 通過模式匹配分配消息的路由鍵屬性,將路由鍵和某個模式進行匹配,此時隊列需要綁定到一個模式上。它將路由鍵和綁定鍵的字符串切分成單詞,這些單詞之間用 點 隔開。它同樣也會識別兩個通配符:符號“#” 和 符號“*”。 #匹配0個或多個單詞 ,* 匹配一個單詞。
安裝 RabbitMQ :我是安裝在linux上,關於安裝,請參考這篇文章 :centos7安裝RabbitMQ
登錄後,可以在網頁上進行簡單操作,比如,創建幾個Exchange,Queues,並進行綁定,然後發送消息,查看Queue裏面接收到的消息。
下面我們用springBoot 來整合RabbitMQ。
在項目pom文件中引入相關依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在yml文件中配置如下:
spring:
rabbitmq:
host: 192.168.xxx.xxx
username: guest
password: guest
port: 5672 # 默認就是5672
事實上,springBoot已經自動裝配了我們操作RabbitMq的模板類RabbitTemplate ,查看 RabbitAutoConfiguration.class ,就可以發現,自動裝配了 RabbitTemplate,我們可以在需要用的地方,直接注入。
默認的模板類的配置如下:
所以,如果RabbitMQ裝在本機,並且沒有修改其端口的話,我們可以直接啓動項目,觀察是否連接。
繼續閱讀代碼,發現還自動裝配了 AmqpAdmin (RabbitMQ系統管理功能組件,創建和刪除 Queue,Exchange功能等)
查看RabbitMessagingTemplate.class ,發現默認使用的消息轉換器 MessageConverter 爲SimpleMessageConverter ,而其中實際使用的是java的序列化機制,如下:
實際項目中,我們更希望 消息以json這種跨平臺,輕量級的形式發送,於是我們可以編寫一個配置類,自定義一個消息轉換器,並注入到容器,使其生效。我們僅需要將其注入到容器中,即可(查看自動裝配的配置類,如果容器中沒有消息轉換器,就自動創建一個SimpleMessageConverter ,如果有就使用容器中自帶的。)
@Configuration
public class MyAMQPConfig {
@Bean // 修改默認的消息轉換器,使用Json的消息轉換器
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
簡單查看了其配置類,下面我們用代碼展示其使用。
發送消息:
// Message 需要 自己構造一個, 定義消息體內容和消息頭
rabbitTemplate.send(exchange,routeKey,message);
// object默認當成消息體,只需要傳入要發送的對象,自動序列化發送給rabbitmq。
rabbitTemplate.convertAndSend(exchange,routeKey,object);
發送消息:
可以使用模板類中的send和convertAndSend方法, 兩者的區別就在於 前者需要自定義消息體內容和消息頭。後者只需要傳入要發送的對象(消息體),自動回序列化(MessageConverter)併發送給rabbitMQ 。例如,我們往名爲“exchange.direct”的交換器上發送一個消息對象,並指定其routingKey爲 zhao.news
@Test
public void contextLoads() {
rabbitTemplate.convertAndSend("exchange.direct", "zhao.news", new Book("西遊記", "吳承恩"));
}
執行該測試方法,發現測試通過,然後我們就可以在頁面上看到隊列裏多了一條數據。
查看消息,如下:
注意看:默認使用的是 java的序列化機制,所以這裏看到的是字節形式,可以按照我上面MyAMQPConfig 配置類,使用json的消息轉換器。 在發送一次消息:
@Test
public void contextLoads() {
Map<String, Object> map = new HashMap<>();
map.put("msg", "這是第一個消息");
map.put("data", Arrays.asList("helloWorld", 1, 2, 3, "test", true));
// 消息的轉換用的是 MessageConverter
rabbitTemplate.convertAndSend("exchange.direct", "zhao.news", map);
}
其接收到的消息如下:
接收消息: receiveAndConvert()
@Test
public void receive() {
Object o = rabbitTemplate.receiveAndConvert("zhao.news");
System.out.println(o.getClass());
System.out.println(o);
}
監聽消息: @EnableRabbit+@RabbitListener
當有這樣的情景時:訂單系統和庫存系統通過消息隊列來傳遞消息時,當下了單後,將訂單信息放入消息隊列後,此時庫存系統怎麼知道該接收消息呢?可以通過監聽消息隊列來實現,當消息隊列中有新的訂單信息,庫存系統就進行庫存的相關操作。
在springBoot中可以使用 @EnableRabbit+@RabbitListener 註解來實現監聽。使用如下:
定義一個service ,用來監聽消息隊列中的消息。
@Service
public class BookService {
@RabbitListener(queues = "zhao.news") //使用該註解,監聽指定的隊列,需要在主啓動類上開啓註解 @EnableRabbit
public void receive(Book book) { // 拿到消息頭中的實體book對象
System.out.println("收到消息:"+book);
}
@RabbitListener(queues = "zhao")
public void receiveMsg(Message message){ //獲取完整的消息,包括消息頭
// System.out.println("監聽隊列完整的信息");
// System.out.println(message.getBody());
// System.out.println(message.getMessageProperties());
}
}
@RabbitListener註解中的queues指定監聽的隊列,也可以監聽多個隊列。
注意:使用該註解,想使得該註解生效,需要在主啓動類上添加註解@EnableRabbit,開啓基於註解的RabbitMQ監聽。
該方法執行於監聽的隊列中的內容變化後。
重新發送一次消息,就會發現控制檯輸出如下:
AmqpAdmin:
RabbitMQ系統管理功能組件,創建和刪除 Queue,Exchange等。前面說過,自動裝配了AmqpAdmin對象,所以我們可以直接注入並使用。
代碼創建Exchange,queue如下:
@Autowired
private AmqpAdmin amqpAdmin; //AmqpAdmin 用來管理組件
@Test
public void createExchange(){// declareXXX用於創建
amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));// 創建direct類型的exchange
System.out.println("創建完成");
amqpAdmin.declareQueue(new Queue("amqpadmin.queue",true));// 名字,是否持久化
// 創建綁定
amqpAdmin.declareBinding(new Binding("amqpAdmin.queue",Binding.DestinationType.QUEUE,
"amqpadmin.exchange","amqp.haha",null));
}
可以在網頁上查看,其隊列的變化。
exchange:
queue:
binding(綁定):
還可以進行刪除操作(deleteXXX) ,這裏就不在展示了。