一、RabbitMQ初識
RabbitMQ是一個實現了高級消息隊列協議(AMQP的消息代理(也叫消息中間件),它接受並轉發消息。它可以幫你處理一些邏輯的事務,從而進行解耦,比如用戶註冊落庫之後,還需要發送郵件驗證、需要發送新人紅包等等事情,就可以交給中間件去做。也可以把它當成一個郵局:當你想郵寄信件的時候,你會把信件放在投遞箱中,並確信郵遞員最終會將信件送到收件人的手裏。在這個例子中,RabbitMQ就相當與投遞箱、郵局和郵遞員。
1.1 AMQP協議
AMQP,即Advanced Message Queuing Protocol,一個提供統一消息服務的應用層標準高級消息隊列協議,是應用層協議的一個開放標準,爲面向消息的中間件設計。基於此協議的客戶端與消息中間件可傳遞消息,並不受客戶端/中間件產品、不同的開發語言等條件的限制。
1.2 典型應用場景
- 跨系統的異步通信 ,異步、解耦、削峯。
- 應用內的同步變成異步 秒殺:自己發送給自己
- 基於Pub/Sub模型實現的事件驅動 ,摒棄ELT(比如全量 同步商戶數據); 摒棄API(比如定時增量獲取用戶、獲取產品,變成增量廣播)。
- 利用RabbitMQ實現事務的最終一致性
1.3 RabbitMQ的特性
RabbitMQ使用Erlang語言編寫,使用Mnesia數據庫存儲消息。
- 可靠性(Reliability) RabbitMQ 使用一些機制來保證可靠性,如持久化、傳輸確認、發佈確認。
- 靈活的路由(Flexible Routing) 在消息進入隊列之前,通過 Exchange 來路由消息的。對於典型的路由功能,RabbitMQ 已經提供了一些內置的 Exchange 來實現。針對更復雜的路由功能,可以將多個 Exchange 綁定在 一起,也通過插件機制實現自己的 Exchange 。
- 消息集羣(Clustering) 多個 RabbitMQ 服務器可以組成一個集羣,形成一個邏輯 Broker 。
- 高可用(Highly Available Queues) 隊列可以在集羣中的機器上進行鏡像,使得在部分節點出問題的情況下
隊列仍然可用。 - 多種協議(Multi-protocol) RabbitMQ 支持多種消息隊列協議,比如 AMQP、STOMP、MQTT 等等。
- 多語言客戶端(Many Clients) RabbitMQ 幾乎支持所有常用語言,比如 Java、.NET、Ruby、PHP、C#、 JavaScript 等等。
- 管理界面(Management UI) RabbitMQ 提供了一個易用的用戶界面,使得用戶可以監控和管理消息、集羣 中的節點。
- 插件機制(Plugin System) RabbitMQ提供了許多插件,以實現從多方面擴展,當然也可以編寫自己的插件。
1.4 工作模型
- Broker
我們要使用 RabbitMQ 來收發消息,必須要安裝一個 RabbitMQ 的服務,可以安裝在 Windows 上面也可以安裝在 Linux 上面,默認是 5672 的端口。這臺 RabbitMQ 的服務器我們把它叫做 Broker, MQ 服務器幫助我們做的事情就是存儲、轉發消息。 - Connection
無論是生產者發送消息,還是消費者接收消息,都必須要跟 Broker 之間建立一個連接,這個連接是一個 TCP 的長連接。 - Channel
如果所有的生產者發送消息和消費者接收消息,都直接創建和釋放 TCP 長連接的話,對於 Broker 來說肯定會造成很大的性能損耗,因爲 TCP 連接是非常寶貴的資源,創建和釋放也要消耗時間。所以在 AMQP 裏面引入了 Channel 的概念,它是一個虛擬的連接。我們把它翻譯成通道,或者消息信道。這樣我們就可以在保持的 TCP 長連接裏面去創建和釋放Channel,大大了減少了資源消耗。另外一個需要注意的是,Channel 是 RabbitMQ 原生 API 裏面的最重要的編程接口,也就是說我們定義交換機、隊列、綁定關係,發送消息消費消息,調用的都是 Channel 接口上的方法。 - Queue
在其他一些 MQ 裏面,比如ActiveMQ ,消息都是發送到隊列上的。隊列是真正用來存儲消息的,是一個獨立運行的進程,有自己的數據庫。消費者獲取消息有兩種模式,一種是 Push 模式,只要生產者發到服務器,就馬上推送給消費者。另一種是 Pull 模式,消息存放在服務端,只有消費者主動獲取才能拿到消息。消費者需要寫一個 while 循環不斷地從隊列獲取消息嗎?不需要,我們可以基於事件機制,實現消費者對隊列的監聽。
由於隊列有 FIFO 的特性,只有確定前一條消息被消費者接收之後,纔會把這條消息從數據庫刪除,繼續投遞下一條消息。 - Exchange
在 RabbitMQ 裏面永遠不會出現消息直接發送到隊列的情況。因爲在 AMQP 裏面引入了交換機(Exchange)的概念,用來實現消息的靈活路由。
交換機是一個綁定列表,用來查找匹配的綁定關係。隊列使用綁定鍵(Binding Key)跟交換機建立綁定關係。生產者發送的消息需要攜帶路由鍵(Routing Key),交換機收到消息時會根據它保存的綁定列表,決定將消息路由到哪些與它綁定的隊列上。注意:交換機與隊列、隊列與消費者都是多對多的關係。 - Vhost
我們每個需要實現基於 RabbitMQ 的異步通信的系統,都需要在服務器上創建自己要用到的交換機、隊列和它們的綁定關係。如果某個業務系統不想跟別人混用一個系統,怎麼辦?再採購一臺硬件服務器單獨安裝一個 RabbitMQ 服務?這種方式成本太高了。在同一個硬件服務器上安裝多個 RabbitMQ 的服務呢?比如再運行一個 5673 的端口?
沒有必要,因爲 RabbitMQ 提供了虛擬主機 VHOST。VHOST 除了可以提高硬件資源的利用率之外,還可以實現資源的隔離和權限的控制。它的作用類似於編程語言中的 namespace 和 package,不同的 VHOST 中可以有同名的 Exchange 和 Queue,它們是完全透明的。
這個時候,我們可以爲不同的業務系統創建不同的用戶(User),然後給這些用戶分配 VHOST 的權限。比如給風控系統的用戶分配風控系統的 VHOST 的權限,這個用戶可以訪問裏面的交換機和隊列。給超級管理員分配所有 VHOST 的權限。 - Producer生產者:主要將消息投遞到對應的Exchange上面。一般是獨立的程序。
- Consumer消費者:消息的接收者,一般是獨立的程序。
1.5 三種主要的交換機
1.5.1 Direct Exchange 直連交換機
定義:直連類型的交換機與一個隊列綁定時,需要指定一個明確的binding key。
路由規則:發送消息到直連類型的交換機時,只有routing key跟binding key完全匹配時,綁定的隊列才能收到消息。
例如:
// 只有隊列1能收到消息
channel.basicPublish("MY_DIRECT_EXCHANGE", "key1", null, msg.getBytes());
1.5.2 Topic Exchange 主題交換機
定義:主題類型的交換機與一個隊列綁定時,可以指定按模式匹配的routing key。
通配符有兩個,*
代表匹配一個單詞。#
代表匹配零個或者多個單詞。單詞與單詞之間用 .
隔開。
路由規則:發送消息到主題類型的交換機時,routing key符合binding key的模式時,綁定的隊列才能收到消息。
例如:
// 只有隊列1能收到消息
channel.basicPublish("MY_TOPIC_EXCHANGE", "ab.123", null, msg.getBytes());
// 隊列2和隊列3能收到消息
channel.basicPublish("MY_TOPIC_EXCHANGE", "bc.abc", null, msg.getBytes());
1.5.2 Fanout Exchange 廣播交換機
定義:廣播類型的交換機與一個隊列綁定時,不需要指定binding key。
路由規則:當消息發送到廣播類型的交換機時,不需要指定routing key,所有與之綁定的隊列都能收到消息。
例如:
// 3個隊列都會收到消息
channel.basicPublish("MY_FANOUT_EXCHANGE", "", null, msg.getBytes());
二、安裝運行
2.1 windows和linux系統
直接官網下載壓縮包,解壓即可,到sbin目錄啓動rabbitmq-server即可。
2.2 mac
mac系統下載安裝包也可以,我是用homebrew安裝的。
brew install rabbitmq
安裝後會有啓動命令。
# 啓動
$ brew services start rabbitmq
# 重啓
$ brew services restart rabbitmq
# 停止
$ brew services stop rabbitmq
啓動完成訪問http://localhost:15672/#/
。看到下面頁面就是安裝成功了。
賬號密碼都是guest。也不用輸入。直接login就行。
三、簡單的生產消費例子
先引入pom依賴。基於springboot的。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3.1 生產者
package com.example.demo;
/**
* @author : pengweiwei
* @date : 2020/1/29 7:09 下午
*/
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.Map;
public class MyProducer {
private final static String QUEUE_NAME = "ORIGIN_QUEUE";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
// 連接IP
factory.setHost("127.0.0.1");
// 連接端口
factory.setPort(5672);
// 虛擬機
factory.setVirtualHost("/");
factory.setUsername("guest");
factory.setPassword("guest");
// 建立連接
Connection conn = factory.newConnection();
// 創建消息通道
Channel channel = conn.createChannel();
String msg = "Hello, RabbitMQ";
// 聲明隊列
/**
* 第一個參數:String queue
* 第二個參數:boolean durable
* 第三個參數:boolean exclusive,
* 第四個參數:boolean autoDelete
* 第五個參數:Map<String, Object> arguments
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 發送消息(發送到默認交換機AMQP Default,Direct) 如果有一個隊列名稱跟Routing Key相等,那麼消息會路由到這個隊列
/**
* 第一個參數:String exchange
* 第二個參數:String routingKey
* 第三個參數:BasicProperties props
* 第四個參數:byte[] body
*/
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
channel.close();
conn.close();
}
}
3.2 消費者
package com.example.demo;
/**
* @author : pengweiwei
* @date : 2020/1/29 7:09 下午
*/
import com.rabbitmq.client.*;
import java.io.IOException;
public class MyConsumer {
private final static String QUEUE_NAME = "ORIGIN_QUEUE";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
// 連接IP
factory.setHost("127.0.0.1");
// 默認監聽端口
factory.setPort(5672);
// 虛擬機
factory.setVirtualHost("/");
// 設置訪問的用戶
factory.setUsername("guest");
factory.setPassword("guest");
// 建立連接
Connection conn = factory.newConnection();
// 創建消息通道
Channel channel = conn.createChannel();
/**
* 聲明隊列
* 第一個參數:String queue
* 第二個參數:boolean durable
* 第三個參數:boolean exclusive,
* 第四個參數:boolean autoDelete
* 第五個參數:Map<String, Object> arguments
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" Waiting for message....");
// 創建消費者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("Received message : '" + msg + "'");
}
};
/**
* 開始獲取消息
* 第一個參數:String queue
* 第二個參數:boolean autoAck
* 第三個參數:Consumer callback
*/
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
2.3 運行結果
2.4 參數說明
聲明交換機的參數
String type:交換機的類型,direct, topic, fanout中的一種。
boolean durable:是否持久化,代表交換機在服務器重啓後是否還存在。
聲明隊列的參數
boolean durable:是否持久化,代表隊列在服務器重啓後是否還存在。
boolean exclusive:是否排他性隊列。排他性隊列只能在聲明它的Connection中使用,連接斷開時自動刪除。
boolean autoDelete:是否自動刪除。如果爲true,至少有一個消費者連接到這個隊列,之後所有與這個隊列連接 的消費者
都斷開時,隊列會自動刪除。
Map<String, Object> arguments:隊列的其他屬性。含義詳細見下表。
屬性 | 含義 |
---|---|
x-message-ttl | 隊列中消息的存活時間,單位毫秒 |
x-expires | 隊列在多久沒有消費者訪問以後會被刪除 |
x-max-length | 隊列的最大消息數 |
x-max-length-bytes | 隊列的最大容量,單位 Byte |
x-dead-letter-exchange | 隊列的死信交換機 |
x-dead-letter-routing-key | 死信交換機的路由鍵 |
x-max-priority | 隊列中消息的最大優先級,消息的優先級不能超過它 |
消息屬性 BasicProperties
主要的參數:
參數 | 釋義 |
---|---|
Map<String,Object> headers | 消息的其他自定義參數 |
Integer deliveryMode | 2表示持久化,其他:瞬態 |
Integer priority | 消息的優先級 |
String correlationId | 關聯 ID,方便 RPC 相應與請求關聯 |
String replyTo | 回調隊列 |
String expiration TTL | , 消息過期時間,單位毫秒 |