文章目錄
1. 頁面發佈
1. 需求分析
業務流程如下:
- 管理員進入管理界面點擊“頁面發佈”,前端請求cms頁面發佈接口
- cms頁面發佈接口執行頁面靜態化,並將靜態化頁面(html文件)存儲至GridFS中
- 靜態化成功後,向消息隊列發送頁面發佈的消息。
- 消息隊列負責將消息發送給各個服務器上部署的Cms Client(Cms客戶端)。
- 每個接收到頁面發佈消息的Cms Client從GridFS獲取Html頁面文件,並將Html文件存儲在本地服務器
2. RabbitMQ研究
消息隊列將頁面發佈的消息通知給各個服務器 ,具體見第二部分
2. RabbitMQ研究
1. 介紹
1. RabbitMQ
MQ全稱Message Queue,消息隊列,RabbitMQ由erlang語言開發,基於AMQP(Advanced Message Queue,高級消息隊列協議)協議實現的消息隊列,是一種應用程序之間的通信方法,在分佈式開發中應用廣泛。
應用場景:1. 任務異步處理,提高應用程序的響應時間;2.應用程序解耦合
RabbitMQ的優勢:
- 使用簡單,功能強大
- 基於AMQP協議
- 社區活躍,文檔完善
- 高併發性能好,得益於Erlang語言
- Spring Boot默認集成RabbitMQ
2. 其他相關知識
AMQP
AMQP是一套公開的消息隊列協議,旨在從協議層定義消息通信數據的標準格式,爲了解決MQ市場上協議不統一的問題。RabbitMQ就是遵循AMQP標準協議開發的MQ服務。
JMS
java提供的一套消息服務API標準,爲了所有的java應用提供統一的消息通信的標準,類似jdbc,和AMQP的區別:jms是java語言專屬的消息服務標準,api層定義標準,只能用於java應用;AMQP是在協議層定義的標準,跨語言。
3. 快速入門
1. RabbitMQ的工作原理
下圖是RabbitMQ的基本結構
組成部分說明:
- Broker:消息隊列服務進程,包括兩部分Exchange和Queue
- Exchange: 消息隊列交換機,按一定規則將消息路由轉發到某個隊列,對消息過濾。
- Queue:消息隊列,存儲消息的隊列,消息到達隊列並轉發給指定的消費方
- Producer:消息生產者,即生產方客戶端,生產方客戶端將消息發送給MQ
- Consumer:消息消費者,即消費方客戶端,接收MQ轉發的消息
消息發佈接收流程:
- 發送消息
- 生產者和Broker建立TCP連接
- 生產者和Broker建立channel通道
- 生產者通過通道消息發送給Broker,由Exchange將消息轉發
- Exchange將消息轉發到指定的Queue(隊列)
- 接收消息
- 消費者和Broker建立TCP連接
- 消費者和Broker建立channel通道
- 消費者監聽指定的Queue(隊列)
- 當有消息到達Queue時Broker默認將消息 推送給消費者
- 消費者接收到消息
2. 下載安裝
RabbitMQ由Erlang語言開發,Erlang語言用於併發及分佈式系統的開發,在電信領域應用廣泛,OTP(Open Telecom Platform)作爲Erlang語言的一部分,包含了很多基於Erlang開發的中間件及工具庫,安裝RabbitMQ需要安裝Erlang/OTP,並保持版本匹配
本項目使用Erlang/OTP 20.3版本和RabbitMQ3.7.3版本
1. 下載安裝
- erlang的下載
erlang安裝完成需要配置erlang環境變量: ERLANG_HOME=D:\Program Files\erl9.3 在path中添加%ERLANG_HOME%\bin;
- RabbitMQ 的安裝
2. 啓動
安裝成功後會自動創建RabbitMQ服務並且啓動
- 安裝並運行服務 rabbitmq-service.bat install 安裝服務 rabbitmq-service.bat stop 停止服務 rabbitmq-service.bat start 啓動服務
- 安裝管理插件 安裝rabbitMQ的管理插件,方便在瀏覽器端管理RabbitMQ 管理員身份運行 rabbitmq-plugins.bat enable rabbitmq_management
- 啓動成功 登錄RabbitMQ 進入瀏覽器,輸入:
http://localhost:15672
初始賬號和密碼:guest/guest
2. hello world
1. 搭建環境
- 創建maven工程 創建生產者工程和消費者工程,分別加入RabbitMQ java client的依賴
生產者和消費者都屬於客戶端,rabbitMQ的java客戶端如下:
test-rabbitmq-producer 生產者工程
test-rabbitmq-consumer :消費者工程
<dependencies>
<dependency>
<!--此版本與spring boot 1.5.9版本匹配-->
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>4.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
2. 生產者
在生產者工程的test中創建測試類
public class Producer01 {
// 隊列
private static final String QUEUE = "helloworld";
public static void main(String[] args) {
// 通過連接工廠創建新的連接和mq建立連接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
connectionFactory.setVirtualHost("/");
// 建立新連接
Connection connection = null;
Channel channel = null;
try {
connection = connectionFactory.newConnection();
// 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
channel = connection.createChannel();
// 聲明隊列
// 參數
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 隊列名稱
* 2. durable:是否持久化,如果持久化,mq重啓後隊列還在
* 3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
* 4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
* 5. arguments:參數,設置一個隊列的擴展參數,如存活時間
*/
channel.queueDeclare(QUEUE,true,false,false,null);
// 發送消息
/**
* String exchange, String routingKey, BasicProperties props, byte[] body
* 1. exchange:交換機,如果不指定使用mq的默認交換機
* 2. routingKey:路由key,交換機根據路由key將消息轉發到指定的隊列,如果使用默認交換機,routingKey設置爲隊列的名稱
* 3. props:消息屬性
* 4. body:消息內容
*/
String message = "hello world 黑馬程序員";
channel.basicPublish("",QUEUE,null,message.getBytes());
System.out.println("send to mq..........");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 先關閉通道
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3. 消費者
消費者工程下的test創建測試類
public class Consumer01 {
// 隊列
private static final String QUEUE = "helloworld";
public static void main(String[] args) throws IOException, TimeoutException {
// 通過連接工廠創建新的連接和mq建立連接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
connectionFactory.setVirtualHost("/");
// 建立新連接
Connection connection = connectionFactory.newConnection();
// 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
Channel channel = connection.createChannel();
// 監聽隊列
// 聲明隊列
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 隊列名稱
* 2. durable:是否持久化,如果持久化,mq重啓後隊列還在
* 3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
* 4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
* 5. arguments:參數,設置一個隊列的擴展參數,如存活時間
*/
channel.queueDeclare(QUEUE,true,false,false,null);
// 監聽隊列
// 實現消費方法
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
// 接收到消息,此方法將被調用
/**
*
* @param consumerTag 消費者標籤,標識消費者,在監聽隊列時設置channel.basicConsume
* @param envelope 信封,通過envelope
* @param properties 消息屬性
* @param body 消息內容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 交換機
String exchange = envelope.getExchange();
// 消息id,mq在channel中用來標識消息的id,可用於確認消息已接收
long deliveryTag = envelope.getDeliveryTag();
// 消息內容
String message = new String(body,"utf-8");
System.out.println("receive message: "+message);
}
};
// 參數:
/**
* String queue, boolean autoAck, Consumer callback
* 1. queue:隊列名稱
* 2. autoAck: 自動回覆,當消費者接收到消息後要告訴mq消息已接收,如果將此參數設置爲true會自動回覆mq,false需要編程實現回覆
* 3. callback: 消費方法,當消費者接受到消息要執行的方法
*/
channel.basicConsume(QUEUE,true,defaultConsumer);
}
}
4. 總結
- 發送端操作流程
- 創建連接
- 創建通道
- 聲明隊列
- 發送消息
- 接收端
- 創建連接
- 創建通道
- 聲明隊列
- 監聽隊列
- 接收消息
- ack回覆
4. 工作模式
有以下幾種工作模式
- Work queues
- Publish/Subscribe
- Routing
- Topics
- Header
- RPC
1. Work queues
與入門程序相比,多了個消費端,兩個消費端共同消費同一個隊列中的消息
應用場景:對於任務過重或任務較多情況使用工作隊列提高任務處理的速度
測試:啓動多個消費者,生產者發送多個消息
結果:
- 一條消息只會被一個消費者接收
- rabbitMQ採用輪詢的方式將消息平均發送給消費者
- 消費者在處理完某條消息後,纔會收到下一條消息
2. Publish/subscribe
1. 工作模式
發佈訂閱模式:
- 每個消費者監聽自己的隊列
- 生產者將消息發給broker,由交換機將消息轉發到綁定此交換機的每個隊列,每個綁定交換機的隊列都將收到消息
2. 代碼
案例:用戶充值成功或轉賬完成系統通知用戶,通知方式有短信和微信公衆號等方法。
- 生產者
public class Producer02_publish {
// 隊列
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
private static final String EXCHANGE_FANOUT_INFORM = "exchange_fanout_inform";
public static void main(String[] args) {
// 通過連接工廠創建新的連接和mq建立連接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
connectionFactory.setVirtualHost("/");
// 建立新連接
Connection connection = null;
Channel channel = null;
try {
connection = connectionFactory.newConnection();
// 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
channel = connection.createChannel();
// 聲明隊列
// 參數
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 隊列名稱
* 2. durable:是否持久化,如果持久化,mq重啓後隊列還在
* 3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
* 4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
* 5. arguments:參數,設置一個隊列的擴展參數,如存活時間
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
// 聲明交換機
/**
* 參數:
* String exchange 交換機的名稱
* String type 交換機的類型
* 1. fanout: 對應的rabbitmq的工作模式是 publish/subscribe
* 2. direct: 對應的routing工作模式
* 3. topic: 對應的topics工作模式
* 4. headers:對應headers工作模式
*/
channel.exchangeDeclare(EXCHANGE_FANOUT_INFORM, BuiltinExchangeType.FANOUT);
// 交換機和隊列綁定
/**
* 參數 String queue, String exchange, String routingKey
* 1. queue 隊列名稱
* 2. exchange 交換機名稱
* 3. routingKey 路由key,作用是交換機根據路由key的值將消息轉發到指定的隊列,發佈訂閱莫斯設爲空字符串
*/
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_FANOUT_INFORM,"");
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_FANOUT_INFORM,"");
// 發送消息
/**
* String exchange, String routingKey, BasicProperties props, byte[] body
* 1. exchange:交換機,如果不指定使用mq的默認交換機
* 2. routingKey:路由key,交換機根據路由key將消息轉發到指定的隊列,如果使用默認交換機,routingKey設置爲隊列的名稱
* 3. props:消息屬性
* 4. body:消息內容
*/
for (int i = 0; i < 5; i++) {
String message = "send inform message to user";
channel.basicPublish(EXCHANGE_FANOUT_INFORM,"",null,message.getBytes());
System.out.println("send to mq..........");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 先關閉通道
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 消費者,郵件消費者
public class Consumer02_subscribe_email {
// 隊列
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String EXCHANGE_FANOUT_INFORM = "exchange_fanout_inform";
public static void main(String[] args) throws IOException, TimeoutException {
// 通過連接工廠創建新的連接和mq建立連接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
connectionFactory.setVirtualHost("/");
// 建立新連接
Connection connection = connectionFactory.newConnection();
// 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
Channel channel = connection.createChannel();
// 監聽隊列
// 聲明隊列
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 隊列名稱
* 2. durable:是否持久化,如果持久化,mq重啓後隊列還在
* 3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
* 4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
* 5. arguments:參數,設置一個隊列的擴展參數,如存活時間
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
// 監聽隊列
// 實現消費方法
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
// 接收到消息,此方法將被調用
/**
*
* @param consumerTag 消費者標籤,標識消費者,在監聽隊列時設置channel.basicConsume
* @param envelope 信封,通過envelope
* @param properties 消息屬性
* @param body 消息內容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 交換機
String exchange = envelope.getExchange();
// 消息id,mq在channel中用來標識消息的id,可用於確認消息已接收
long deliveryTag = envelope.getDeliveryTag();
// 消息內容
String message = new String(body,"utf-8");
System.out.println("receive message: "+message);
}
};
// 參數:
/**
* String queue, boolean autoAck, Consumer callback
* 1. queue:隊列名稱
* 2. autoAck: 自動回覆,當消費者接收到消息後要告訴mq消息已接收,如果將此參數設置爲true會自動回覆mq,false需要編程實現回覆
* 3. callback: 消費方法,當消費者接受到消息要執行的方法
*/
channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
}
}
- 短信發送消費者,同上面類似
3. 測試
生產者發送的消息,每條都轉發到了各個隊列,每個消費者都接收到消息
4. publish/subscribe 和 work queues的區別
區別:
- work queues不用定義交換機,而publish/subscribe需要定義交換機
- publish/subscribe的生產方是面向交換機發送消息,work queues的生產方是面向隊列發送消息(底層使用默認交換機)
- publish/subscribe需要設置隊列和交換機的綁定,work queues不需要設置,實質上work queues會將隊列綁定到默認的交換機
相同點:
兩者實現的發佈/訂閱的效果是一樣的,多個消費端監聽同一個隊列不會重複消費消息
**建議使用 publish/subscribe,**發佈訂閱模式比工作隊列模式更強大,並且發佈訂閱模式可以指定自己專用的交換機。
3. Routing
1. 工作模式
路由模式:
- 每個消費者監聽自己的隊列,並且設置routingkey
- 生產者將消息發給交換機,由交換機根據routingkey轉發消息給指定隊列
2. 代碼
- 生產者
聲明exchange_routing_inform交換機
聲明兩個隊列並且綁定到此交換機,綁定時需要指定routingkey
發送消息時需要指定routingkey
public class Producer03_routing {
// 隊列
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
private static final String EXCHANGE_ROUTING_INFORM = "exchange_routing_inform";
private static final String ROUTINGKEY_EMAIL = "inform_email";
private static final String ROUTINGKEY_SMS = "inform_sms";
public static void main(String[] args) {
// 通過連接工廠創建新的連接和mq建立連接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
connectionFactory.setVirtualHost("/");
// 建立新連接
Connection connection = null;
Channel channel = null;
try {
connection = connectionFactory.newConnection();
// 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
channel = connection.createChannel();
// 聲明隊列
// 參數
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 隊列名稱
* 2. durable:是否持久化,如果持久化,mq重啓後隊列還在
* 3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
* 4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
* 5. arguments:參數,設置一個隊列的擴展參數,如存活時間
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
// 聲明交換機
/**
* 參數:
* String exchange 交換機的名稱
* String type 交換機的類型
* 1. fanout: 對應的rabbitmq的工作模式是 publish/subscribe
* 2. direct: 對應的routing工作模式
* 3. topic: 對應的topics工作模式
* 4. headers:對應headers工作模式
*/
channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT);
// 交換機和隊列綁定
/**
* 參數 String queue, String exchange, String routingKey
* 1. queue 隊列名稱
* 2. exchange 交換機名稱
* 3. routingKey 路由key,作用是交換機根據路由key的值將消息轉發到指定的隊列,發佈訂閱莫斯設爲空字符串
*/
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_SMS);
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_ROUTING_INFORM,"inform");
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL);
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,"inform");
// 發送消息
/**
* String exchange, String routingKey, BasicProperties props, byte[] body
* 1. exchange:交換機,如果不指定使用mq的默認交換機
* 2. routingKey:路由key,交換機根據路由key將消息轉發到指定的隊列,如果使用默認交換機,routingKey設置爲隊列的名稱
* 3. props:消息屬性
* 4. body:消息內容
*/
/* for (int i = 0; i < 5; i++) {
String message = "send email inform message to user";
channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL,null,message.getBytes());
System.out.println("send to mq..........");
}
for (int i = 0; i < 5; i++) {
String message = "send sms inform message to user";
channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_SMS,null,message.getBytes());
System.out.println("send to mq..........");
}*/
for (int i = 0; i < 5; i++) {
String message = "send inform message to user";
channel.basicPublish(EXCHANGE_ROUTING_INFORM,"inform",null,message.getBytes());
System.out.println("send to mq..........");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 先關閉通道
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 郵件發送消費者
public class Consumer03_routing_email {
// 隊列
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String EXCHANGE_ROUTING_INFORM = "exchange_routing_inform";
private static final String ROUTINGKEY_EMAIL = "inform_email";
public static void main(String[] args) throws IOException, TimeoutException {
// 通過連接工廠創建新的連接和mq建立連接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
connectionFactory.setVirtualHost("/");
// 建立新連接
Connection connection = connectionFactory.newConnection();
// 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
Channel channel = connection.createChannel();
// 監聽隊列
// 聲明隊列
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 隊列名稱
* 2. durable:是否持久化,如果持久化,mq重啓後隊列還在
* 3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
* 4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
* 5. arguments:參數,設置一個隊列的擴展參數,如存活時間
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
// 聲明交換機
/**
* 參數:
* String exchange 交換機的名稱
* String type 交換機的類型
* 1. fanout: 對應的rabbitmq的工作模式是 publish/subscribe
* 2. direct: 對應的routing工作模式
* 3. topic: 對應的topics工作模式
* 4. headers:對應headers工作模式
*/
channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT);
// 綁定隊列
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL);
// 監聽隊列
// 實現消費方法
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
// 接收到消息,此方法將被調用
/**
*
* @param consumerTag 消費者標籤,標識消費者,在監聽隊列時設置channel.basicConsume
* @param envelope 信封,通過envelope
* @param properties 消息屬性
* @param body 消息內容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 交換機
String exchange = envelope.getExchange();
// 消息id,mq在channel中用來標識消息的id,可用於確認消息已接收
long deliveryTag = envelope.getDeliveryTag();
// 消息內容
String message = new String(body,"utf-8");
System.out.println("receive message: "+message);
}
};
// 參數:
/**
* String queue, boolean autoAck, Consumer callback
* 1. queue:隊列名稱
* 2. autoAck: 自動回覆,當消費者接收到消息後要告訴mq消息已接收,如果將此參數設置爲true會自動回覆mq,false需要編程實現回覆
* 3. callback: 消費方法,當消費者接受到消息要執行的方法
*/
channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
}
}
- 短信發送消費者,類似上面郵件的代碼
3. 測試
打開RabbitMQ的管理界面,觀察交換機綁定情況
有routingkey,根據routingkey轉發消息到指定的隊列
4. 思考
Routing模式和Publish/subscibe的區別: Routing模式要求隊列在綁定交換機時要指定routingkey,消息會轉發到符合routingkey的隊列。
4. topics
1. 工作模式
路由模式:
- 每個消費者監聽自己的隊列,並且設置帶通配符的routingkey
- 生產者將消息發給broker,由交換機根據routingkey轉發消息到指定隊列
2. 代碼
案例:
根據用戶的通知設置去通知用戶,設置接收Email的用戶只接收email,設置接收sms的用戶只接收sms,設置兩種都接收的則兩種都有效。
- 生產者
聲明交換機,指定topic類型
public class Producer04_topics {
// 隊列
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
private static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";
private static final String ROUTINGKEY_EMAIL = "inform.#.email.#";
private static final String ROUTINGKEY_SMS = "inform.#.sms.#";
public static void main(String[] args) {
// 通過連接工廠創建新的連接和mq建立連接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
connectionFactory.setVirtualHost("/");
// 建立新連接
Connection connection = null;
Channel channel = null;
try {
connection = connectionFactory.newConnection();
// 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
channel = connection.createChannel();
// 聲明隊列
// 參數
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 隊列名稱
* 2. durable:是否持久化,如果持久化,mq重啓後隊列還在
* 3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
* 4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
* 5. arguments:參數,設置一個隊列的擴展參數,如存活時間
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
// 聲明交換機
/**
* 參數:
* String exchange 交換機的名稱
* String type 交換機的類型
* 1. fanout: 對應的rabbitmq的工作模式是 publish/subscribe
* 2. direct: 對應的routing工作模式
* 3. topic: 對應的topics工作模式
* 4. headers:對應headers工作模式
*/
channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC);
// 交換機和隊列綁定
/**
* 參數 String queue, String exchange, String routingKey
* 1. queue 隊列名稱
* 2. exchange 交換機名稱
* 3. routingKey 路由key,作用是交換機根據路由key的值將消息轉發到指定的隊列,發佈訂閱莫斯設爲空字符串
*/
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_SMS);
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_TOPICS_INFORM,"inform");
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_EMAIL);
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,"inform");
// 發送消息
/**
* String exchange, String routingKey, BasicProperties props, byte[] body
* 1. exchange:交換機,如果不指定使用mq的默認交換機
* 2. routingKey:路由key,交換機根據路由key將消息轉發到指定的隊列,如果使用默認交換機,routingKey設置爲隊列的名稱
* 3. props:消息屬性
* 4. body:消息內容
*/
for (int i = 0; i < 5; i++) {
String message = "send email inform message to user";
channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.email",null,message.getBytes());
System.out.println("send to mq..........");
}
/* for (int i = 0; i < 5; i++) {
String message = "send sms inform message to user";
channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.sms",null,message.getBytes());
System.out.println("send to mq..........");
}*/
for (int i = 0; i < 5; i++) {
String message = "send sms and email inform message to user";
channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.sms.email",null,message.getBytes());
System.out.println("send to mq..........");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 先關閉通道
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 消費端
隊列綁定交換機指定通配符
通配符規則:
- 中間以
.
分隔 - 符號
#
可以匹配多個詞,符號*
可以匹配一個詞
public class Consumer04_topics_email {
// 隊列
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";
private static final String ROUTINGKEY_EMAIL = "inform.#.email.#";
public static void main(String[] args) throws IOException, TimeoutException {
// 通過連接工廠創建新的連接和mq建立連接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
// 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
connectionFactory.setVirtualHost("/");
// 建立新連接
Connection connection = connectionFactory.newConnection();
// 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
Channel channel = connection.createChannel();
// 監聽隊列
// 聲明隊列
/**
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
* 1. queue: 隊列名稱
* 2. durable:是否持久化,如果持久化,mq重啓後隊列還在
* 3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
* 4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
* 5. arguments:參數,設置一個隊列的擴展參數,如存活時間
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
// 聲明交換機
/**
* 參數:
* String exchange 交換機的名稱
* String type 交換機的類型
* 1. fanout: 對應的rabbitmq的工作模式是 publish/subscribe
* 2. direct: 對應的routing工作模式
* 3. topic: 對應的topics工作模式
* 4. headers:對應headers工作模式
*/
channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC);
// 綁定隊列
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_EMAIL);
// 監聽隊列
// 實現消費方法
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
// 接收到消息,此方法將被調用
/**
*
* @param consumerTag 消費者標籤,標識消費者,在監聽隊列時設置channel.basicConsume
* @param envelope 信封,通過envelope
* @param properties 消息屬性
* @param body 消息內容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
// 交換機
String exchange = envelope.getExchange();
// 消息id,mq在channel中用來標識消息的id,可用於確認消息已接收
long deliveryTag = envelope.getDeliveryTag();
// 消息內容
String message = new String(body,"utf-8");
System.out.println("receive message: "+message);
}
};
// 參數:
/**
* String queue, boolean autoAck, Consumer callback
* 1. queue:隊列名稱
* 2. autoAck: 自動回覆,當消費者接收到消息後要告訴mq消息已接收,如果將此參數設置爲true會自動回覆mq,false需要編程實現回覆
* 3. callback: 消費方法,當消費者接受到消息要執行的方法
*/
channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
}
}
3. 測試
生產者發送若干條消息,交換機根據routekey通配符匹配並轉發到指定的隊列
4. 思考
需求可否由routing工作模式實現?
使用routing模式也可以實現,但是過程比topics複雜
topic模式更加強大,可以實現routing、publish/subscribe模式的功能
5. header模式
header模式取消routingkey,使用header中的key/value匹配隊列
案例:
根據用戶的通知設置去通知用戶,設置接收Email的用戶只接收Email,設置接收sms的用戶只接收sms,設置兩種通知類型都接收的則兩種通知都有效。
代碼:
- 生產者
隊列和交換機的綁定的代碼
Map<String, Object> headers_email = new Hashtable<String, Object>();
headers_email.put("inform_type", "email");
Map<String, Object> headers_sms = new Hashtable<String, Object>();
headers_sms.put("inform_type", "sms");
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_HEADERS_INFORM,"",headers_email);
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_HEADERS_INFORM,"",headers_sms);
通知
String message = "email inform to user"+i;
Map<String,Object> headers = new Hashtable<String, Object>();
headers.put("inform_type", "email");//匹配email通知消費者綁定的header
//headers.put("inform_type", "sms");//匹配sms通知消費者綁定的header
AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties.Builder();
properties.headers(headers);
//Email通知
channel.basicPublish(EXCHANGE_HEADERS_INFORM, "", properties.build(), message.getBytes());
- 消費者
channel.exchangeDeclare(EXCHANGE_HEADERS_INFORM, BuiltinExchangeType.HEADERS);
Map<String, Object> headers_email = new Hashtable<String, Object>();
headers_email.put("inform_email", "email");
//交換機和隊列綁定
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_HEADERS_INFORM,"",headers_email);
//指定消費隊列
channel.basicConsume(QUEUE_INFORM_EMAIL, true, consumer);
3. 測試
6. PRC
RPC即客戶端遠程調用服務端的方法 ,使用MQ可以實現RPC的異步調用,基於Direct交換機實現 ,流程:
- 客戶端既是生產者也是消費者,向RPC請求隊列發送RPC調用消息,同時監聽RPC響應隊列
- 服務端監聽RPC請求隊列的消息,收到消息後執行服務端的方法,得到方法返回的結果
- 服務端將RPC方法 的結果發送到RPC響應隊列
- 客戶端(RPC調用方)監聽RPC響應隊列,接收到RPC調用結果。
5. spring整合RabbitMQ
1. 搭建springboot環境
基於springboot操作RabbitMQ
<dependencies>
<!--<dependency>
<!–此版本與spring boot 1.5.9版本匹配–>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>4.0.3</version>
</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
2. 配置
1. 配置application.yml
server:
port: 44000
spring:
application:
name: test-rabbitmq-producer
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtualHost: /
2. 定義RabbitConfig類
配置Exchange Queue以及綁定交換機
package com.xuecheng.test.rabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitmqConfig {
// 隊列
public static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
public static final String QUEUE_INFORM_SMS = "queue_inform_sms";
public static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";
public static final String ROUTINGKEY_EMAIL = "inform.#.email.#";
public static final String ROUTINGKEY_SMS = "inform.#.sms.#";
// 聲明交換機
@Bean(EXCHANGE_TOPICS_INFORM)
public Exchange EXCHANGE_TOPICS_INFORM(){
// durable 持久化
return ExchangeBuilder.topicExchange(EXCHANGE_TOPICS_INFORM).durable(false).build();
}
// 聲明隊列QUEUE_INFORM_EMAIL
@Bean(QUEUE_INFORM_EMAIL)
public Queue QUEUE_INFORM_EMAIL(){
return new Queue(QUEUE_INFORM_EMAIL);
}
// 聲明隊列QUEUE_INFORM_SMS
@Bean(QUEUE_INFORM_SMS)
public Queue QUEUE_INFORM_SMS(){
return new Queue(QUEUE_INFORM_SMS);
}
// 綁定email交換機和隊列,指定routingkey
@Bean
public Binding BINDING_QUEUE_INFORM_EMAIL(@Qualifier(QUEUE_INFORM_EMAIL) Queue queue,
@Qualifier(EXCHANGE_TOPICS_INFORM)Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_EMAIL).noargs();
}
// 綁定sms交換機和隊列,指定routingkey
@Bean
public Binding BINDING_QUEUE_INFORM_SMS(@Qualifier(QUEUE_INFORM_SMS) Queue queue,
@Qualifier(EXCHANGE_TOPICS_INFORM)Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_SMS).noargs();
}
}
3. 生產端
使用RabbitTemplate發送消息
@RunWith(SpringRunner.class)
@SpringBootTest
public class Producer05_topics_springboot {
@Autowired
private RabbitTemplate rabbitTemplate;
// 使用rabbittemplate發送消息
@Test
public void testSendEmail(){
String message = "send email message to user";
/**
* 參數
* 1. 交換機名稱
* 2. routingkey
* 3. 消息內容
*/
rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE_TOPICS_INFORM,"inform.email",message);
}
}
4. 消費端
創建消費端工程,添加依賴,和生產端一樣
使用@RabbitListener
註解監聽隊列
@Component
public class ReceiveHandler {
@RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_EMAIL})
public void send_email(String msg, Message message, Channel channel){
System.out.println("receive message is: "+msg);
}
}