RabbitMQ系列(三)RabbitMQ交換器Exchange介紹與實踐

王磊的部落格發表於2018-07-11

導讀

有了Rabbit的基礎知識之後(基礎知識詳見:深入解讀RabbitMQ工作原理及簡單使用),本章我們重點學習一下Rabbit裡面的exchange(交換器)的知識。

交換器分類

RabbitMQ的Exchange(交換器)分為四類:

  • direct(預設)
  • headers
  • fanout
  • topic

其中headers交換器允許你匹配AMQP訊息的header而非路由鍵,除此之外headers交換器和direct交換器完全一致,但效能卻很差,幾乎用不到,所以我們本文也不做講解。

注意: fanout、topic交換器是沒有歷史資料的,也就是說對於中途建立的佇列,獲取不到之前的訊息。

1、direct交換器

direct為預設的交換器型別,也非常的簡單,如果路由鍵匹配的話,訊息就投遞到相應的佇列,如圖:

RabbitMQ系列(三)RabbitMQ交換器Exchange介紹與實踐

使用程式碼:channel.basicPublish("", QueueName, null, message)推送direct交換器訊息到對於的佇列,空字元為預設的direct交換器,用佇列名稱當做路由鍵。

direct交換器程式碼示例

傳送端:

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
// 宣告佇列【引數說明:引數一:佇列名稱,引數二:是否持久化;引數三:是否獨佔模式;引數四:消費者斷開連線時是否刪除佇列;引數五:訊息其他引數】
channel.queueDeclare(config.QueueName, false, false, false, null);
String message = String.format("當前時間:%s", new Date().getTime());
// 推送內容【引數說明:引數一:交換機名稱;引數二:佇列名稱,引數三:訊息的其他屬性-路由的headers資訊;引數四:訊息主體】
channel.basicPublish("", config.QueueName, null, message.getBytes("UTF-8"));
複製程式碼

接收端,持續接收訊息:

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
// 宣告佇列【引數說明:引數一:佇列名稱,引數二:是否持久化;引數三:是否獨佔模式;引數四:消費者斷開連線時是否刪除佇列;引數五:訊息其他引數】
channel.queueDeclare(config.QueueName, false, false, false, null);
Consumer defaultConsumer = new DefaultConsumer(channel) {
	@Override
	public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
			byte[] body) throws IOException {
		String message = new String(body, "utf-8"); // 訊息正文
		System.out.println("收到訊息 => " + message);
		channel.basicAck(envelope.getDeliveryTag(), false); // 手動確認訊息【引數說明:引數一:該訊息的index;引數二:是否批量應答,true批量確認小於當前id的訊息】
	}
};
channel.basicConsume(config.QueueName, false, "", defaultConsumer);
複製程式碼

接收端,獲取單條訊息

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.queueDeclare(config.QueueName, false, false, false, null);
GetResponse resp = channel.basicGet(config.QueueName, false);
String message = new String(resp.getBody(), "UTF-8");
channel.basicAck(resp.getEnvelope().getDeliveryTag(), false); // 訊息確認
複製程式碼

持續訊息獲取使用:basic.consume;單個訊息獲取使用:basic.get。

注意: 不能使用for迴圈單個訊息消費來替代持續訊息消費,因為這樣效能很低;

公平排程

當接收端訂閱者有多個的時候,direct會輪詢公平的分發給每個訂閱者(訂閱者訊息確認正常),如圖:

RabbitMQ系列(三)RabbitMQ交換器Exchange介紹與實踐

訊息的發後既忘特性

發後既忘模式是指接受者不知道訊息的來源,如果想要指定訊息的傳送者,需要包含在傳送內容裡面,這點就像我們在信件裡面註明自己的姓名一樣,只有這樣才能知道傳送者是誰。

訊息確認

看了上面的程式碼我們可以知道,訊息接收到之後必須使用channel.basicAck()方法手動確認(非自動確認刪除模式下),那麼問題來了。

訊息收到未確認會怎麼樣?

如果應用程式接收了訊息,因為bug忘記確認接收的話,訊息在佇列的狀態會從“Ready”變為“Unacked”,如圖:

RabbitMQ系列(三)RabbitMQ交換器Exchange介紹與實踐

如果訊息收到卻未確認,Rabbit將不會再給這個應用程式傳送更多的訊息了,這是因為Rabbit認為你沒有準備好接收下一條訊息。

此條訊息會一直保持Unacked的狀態,直到你確認了訊息,或者斷開與Rabbit的連線,Rabbit會自動把訊息改完Ready狀態,分發給其他訂閱者。

當然你可以利用這一點,讓你的程式延遲確認該訊息,直到你的程式處理完相應的業務邏輯,這樣可以有效的防治Rabbit給你過多的訊息,導致程式崩潰。

訊息確認Demo:

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.queueDeclare(config.QueueName, false, false, false, null);
GetResponse resp = channel.basicGet(config.QueueName, false);
String message = new String(resp.getBody(), "UTF-8");
channel.basicAck(resp.getEnvelope().getDeliveryTag(), false);
複製程式碼

channel.basicAck(long deliveryTag, boolean multiple)為訊息確認,引數1:訊息的id;引數2:是否批量應答,true批量確認小於次id的訊息。

總結:消費者消費的每條訊息都必須確認。

訊息拒絕

訊息在確認之前,可以有兩個選擇:

選擇1:斷開與Rabbit的連線,這樣Rabbit會重新把訊息分派給另一個消費者;

選擇2:拒絕Rabbit傳送的訊息使用channel.basicReject(long deliveryTag, boolean requeue),引數1:訊息的id;引數2:處理訊息的方式,如果是true,Rabbib會重新分配這個訊息給其他訂閱者,如果設定成false的話,Rabbit會把訊息傳送到一個特殊的“死信”佇列,用來存放被拒絕而不重新放入佇列的訊息。

訊息拒絕Demo:

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.queueDeclare(config.QueueName, false, false, false, null);
GetResponse resp = channel.basicGet(config.QueueName, false);
String message = new String(resp.getBody(), "UTF-8");
channel.basicReject(resp.getEnvelope().getDeliveryTag(), true); //訊息拒絕
複製程式碼

2、fanout交換器——釋出/訂閱模式

fanout有別於direct交換器,fanout是一種釋出/訂閱模式的交換器,當你傳送一條訊息的時候,交換器會把訊息廣播到所有附加到這個交換器的佇列上。

比如使用者上傳了自己的頭像,這個時候圖片需要清除快取,同時使用者應該得到積分獎勵,你可以把這兩個佇列繫結到圖片上傳的交換器上,這樣當有第三個、第四個上傳完圖片需要處理的需求的時候,原來的程式碼可以不變,只需要新增一個訂閱訊息即可,這樣傳送方和消費者的程式碼完全解耦,並可以輕而易舉的新增新功能了。

和direct交換器不同,我們在傳送訊息的時候新增channel.exchangeDeclare(ExchangeName, "fanout"),這行程式碼宣告fanout交換器。

傳送端:

final String ExchangeName = "fanoutec"; // 交換器名稱
Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.exchangeDeclare(ExchangeName, "fanout"); // 宣告fanout交換器
String message = "時間:" + new Date().getTime();
channel.basicPublish(ExchangeName, "", null, message.getBytes("UTF-8"));
複製程式碼

接受訊息不同於direct,我們需要宣告fanout路由器,並使用預設的佇列繫結到fanout交換器上。

接收端:

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.exchangeDeclare(ExchangeName, "fanout"); // 宣告fanout交換器
String queueName = channel.queueDeclare().getQueue(); // 宣告佇列
channel.queueBind(queueName, ExchangeName, "");
Consumer consumer = new DefaultConsumer(channel) {
	@Override
	public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
			byte[] body) throws IOException {
		String message = new String(body, "UTF-8");
	}
};
channel.basicConsume(queueName, true, consumer);
複製程式碼

fanout和direct的區別最多的在接收端,fanout需要繫結佇列到對應的交換器用於訂閱訊息。

其中channel.queueDeclare().getQueue()為隨機佇列,Rabbit會隨機生成佇列名稱,一旦消費者斷開連線,該佇列會自動刪除。

注意: 對於fanout交換器來說routingKey(路由鍵)是無效的,這個引數是被忽略的。

3、topic交換器——匹配訂閱模式

最後介紹的是topic交換器,topic交換器執行和fanout類似,但是可以更靈活的匹配自己想要訂閱的資訊,這個時候routingKey路由鍵就排上用場了,使用路由鍵進行訊息(規則)匹配。

假設我們現在有一個日誌系統,會把所有日誌級別的日誌傳送到交換器,warning、log、error、fatal,但我們只想處理error以上的日誌,要怎麼處理?這就需要使用topic路由器了。

topic路由器的關鍵在於定義路由鍵,定義routingKey名稱不能超過255位元組,使用“.”作為分隔符,例如:com.mq.rabbit.error。

消費訊息的時候routingKey可以使用下面字元匹配訊息:

  • "*"匹配一個分段(用“.”分割)的內容;
  • "#"匹配0和多個字元;

例如釋出了一個“com.mq.rabbit.error”的訊息:

能匹配上的路由鍵:

  • cn.mq.rabbit.*
  • cn.mq.rabbit.#
  • #.error
  • cn.mq.#
  • #

不能匹配上的路由鍵:

所以如果想要訂閱所有訊息,可以使用“#”匹配。

注意: fanout、topic交換器是沒有歷史資料的,也就是說對於中途建立的佇列,獲取不到之前的訊息。

釋出端:

String routingKey = "com.mq.rabbit.error";
Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.exchangeDeclare(ExchangeName, "topic"); // 宣告topic交換器
String message = "時間:" + new Date().getTime();
channel.basicPublish(ExchangeName, routingKey, null, message.getBytes("UTF-8"));
複製程式碼

接收端:

Connection conn = connectionFactoryUtil.GetRabbitConnection();
Channel channel = conn.createChannel();
channel.exchangeDeclare(ExchangeName, "topic"); // 宣告topic交換器
String queueName = channel.queueDeclare().getQueue(); // 宣告佇列
String routingKey = "#.error";
channel.queueBind(queueName, ExchangeName, routingKey);
Consumer consumer = new DefaultConsumer(channel) {
	@Override
	public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
			byte[] body) throws IOException {
		String message = new String(body, "UTF-8");
		System.out.println(routingKey + "|接收訊息 => " + message);
	}
};
channel.basicConsume(queueName, true, consumer);
複製程式碼

擴充套件部分—自定義執行緒池

如果需要更大的控制連線,使用者可自己設定執行緒池,程式碼如下:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

ExecutorService es = Executors.newFixedThreadPool(20);
Connection conn = factory.newConnection(es);
複製程式碼

其實看過原始碼的同學可能知道,factory.newConnection本身預設也有執行緒池的機制,ConnectionFactory.class部分原始碼如下:

private ExecutorService sharedExecutor;
public Connection newConnection() throws IOException, TimeoutException {
		return newConnection(this.sharedExecutor, Collections.singletonList(new Address(getHost(), getPort())));
}
public void setSharedExecutor(ExecutorService executor) {
		this.sharedExecutor = executor;
}
複製程式碼

其中this.sharedExecutor就是預設的執行緒池,可以通過setSharedExecutor()方法設定ConnectionFactory的執行緒池,如果不設定則為null。

使用者如果自己設定了執行緒池,像本小節第一段程式碼寫的那樣,那麼當連線關閉的時候,不會自動關閉使用者自定義的執行緒池,所以使用者必須自己手動關閉,通過呼叫shutdown()方法,否則可能會阻止JVM的終止。

官方的建議是隻有在程式出現嚴重效能瓶頸的時候,才應該考慮使用此功能。

專案地址

GitHub:github.com/vipstone/ra…

RabbitMQ系列(三)RabbitMQ交換器Exchange介紹與實踐

相關文章