rabbitMq消息模型
傳遞一個信息給多個消費者。 這種模式被稱爲“發佈/訂閱”。
1. 發佈-訂閱模式介紹
1、1個生產者,多個消費者
2、每一個消費者都有自己的一個隊列
3、生產者沒有將消息直接發送到隊列,而是發送到了交換機
4、每個隊列都要綁定到交換機
5、生產者發送的消息,經過交換機到達隊列,實現一個消息被多個消費者獲取的目的
X(Exchanges):交換機一方面:接收生產者發送的消息。另一方面:知道如何處理消息,例如遞交給某個特別隊列、遞交給所有隊列、或是將消息丟棄。到底如何操作,取決於Exchange的類型。
Exchange類型有以下幾種:
Fanout:廣播,將消息交給所有綁定到交換機的隊列
Direct:定向,把消息交給符合指定routing key 的隊列
Topic:通配符,把消息交給符合routing pattern(路由模式) 的隊列
Exchange(交換機)只負責轉發消息,不具備存儲消息的能力,因此如果沒有任何隊列與Exchange綁定,或者沒有符合路由規則的隊列,那麼消息會丟失!
2. 訂閱模型-Fanout
在廣播模式下,消息發送流程是這樣的:
- 1) 可以有多個消費者
- 2) 每個消費者有自己的queue(隊列)
- 3) 每個隊列都要綁定到Exchange(交換機)
- 4) 生產者發送的消息,只能發送到交換機,交換機來決定要發給哪個隊列,生產者無法決定。
- 5) 交換機把消息發送給綁定過的所有隊列
- 6) 隊列的消費者都能拿到消息。實現一條消息被多個消費者消費
2.1 生產者
兩個變化:
- 1) 聲明Exchange,不再聲明Queue
- 2) 發送消息到Exchange,不再發送到Queue
public class Send {
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 獲取通道
Channel channel = connection.createChannel();
// 聲明exchange,指定類型爲fanout
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 消息內容
String message = "Hello everyone";
// 發佈消息到Exchange
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println(" [生產者] Sent '" + message + "'");
channel.close();
connection.close();
}
}
2.2 消費者1
public class Recv {
private final static String QUEUE_NAME = "fanout_exchange_queue_1";
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 獲取通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 綁定隊列到交換機
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 定義隊列的消費者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 獲取消息,並且處理,這個方法類似事件監聽,如果有消息的時候,會被自動調用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息體
String msg = new String(body);
System.out.println(" [消費者1] received : " + msg + "!");
}
};
// 監聽隊列,自動返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
2.2 消費者2
public class Recv2 {
private final static String QUEUE_NAME = "fanout_exchange_queue_2";
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 獲取通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 綁定隊列到交換機
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 定義隊列的消費者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 獲取消息,並且處理,這個方法類似事件監聽,如果有消息的時候,會被自動調用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息體
String msg = new String(body);
System.out.println(" [消費者2] received : " + msg + "!");
}
};
// 監聽隊列,手動返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
2.3 測試
運行兩個消費者,然後發送1條消息:
3. 訂閱模型-Direct
有選擇性的接收消息
在訂閱模式中,生產者發佈消息,所有消費者都可以獲取所有消息。
在路由模式中,將添加一個功能 - 我們將只能訂閱一部分消息。 例如,我們只能將重要的錯誤消息引導到日誌文件(以節省磁盤空間),同時仍然能夠在控制檯上打印所有日誌消息。
但是,在某些場景下,我們希望不同的消息被不同的隊列消費。這時就要用到Direct類型的Exchange。
在Direct模型下,隊列與交換機的綁定,不能是任意綁定,而是要指定一個RoutingKey(路由key)
消息的發送方在向Exchange發送消息時,也必須指定消息的routing key。
P:生產者,向Exchange發送消息,發送消息時,會指定一個routing key。
X:Exchange(交換機),接收生產者的消息,然後把消息遞交給 與routing key完全匹配的隊列
C1:消費者,其所在隊列指定了需要routing key 爲 error 的消息
C2:消費者,其所在隊列指定了需要routing key 爲 info、error、warning 的消息
3.1 生產者
此處我們模擬java中黑名單的增刪改,發送消息的RoutingKey分別是:insert、update、delete
public class Send {
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 獲取通道
Channel channel = connection.createChannel();
// 聲明exchange,指定類型爲direct
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 消息內容
String message = "新增, id = 1001";
// 發送消息,並且指定routing key 爲:insert ,代表新增黑名單對象
channel.basicPublish(EXCHANGE_NAME, "insert", null, message.getBytes());
System.out.println(" [覈驗黑名單服務:] Sent '" + message + "'");
channel.close();
connection.close();
}
}
3.2 消費者1
此處假設消費者1只接收兩種類型的消息:更新和刪除黑名單對象。
public class Recv {
private final static String QUEUE_NAME = "direct_exchange_queue_1";
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 獲取通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 綁定隊列到交換機,同時指定需要訂閱的routing key。假設此處需要update和delete消息
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
// 定義隊列的消費者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 獲取消息,並且處理,這個方法類似事件監聽,如果有消息的時候,會被自動調用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息體
String msg = new String(body);
System.out.println(" [消費者1] received : " + msg + "!");
}
};
// 監聽隊列,自動ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
3.3 消費者2
public class Recv2 {
private final static String QUEUE_NAME = "direct_exchange_queue_2";
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 獲取通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 綁定隊列到交換機,同時指定需要訂閱的routing key。訂閱 insert、update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
// 定義隊列的消費者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 獲取消息,並且處理,這個方法類似事件監聽,如果有消息的時候,會被自動調用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息體
String msg = new String(body);
System.out.println(" [消費者2] received : " + msg + "!");
}
};
// 監聽隊列,自動ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
3.4 測試
生產者發送了一次刪除,一次新增,
消費者一中沒有綁定insert , 所以消費者一不會受到新增的消息,消費者二則都可以收到
如下:
4. 訂閱模型-Topic
Topic
類型的Exchange
與Direct
相比,都是可以根據RoutingKey
把消息路由到不同的隊列。只不過Topic
類型Exchange
可以讓隊列在綁定Routing key
的時候使用通配符!
Routingkey
一般都是有一個或多個單詞組成,多個單詞之間以”.”分割,例如: audit.insert
通配符規則:
#
:匹配一個或多個詞
*
:匹配不多不少恰好1個詞
舉例:
audit.#
:能夠匹配audit.irs.corporate
或者 audit.irs
audit.*
:只能匹配audit.irs
在這個例子中,我們將發送所有描述動物的消息。消息將使用由三個字(兩個點)組成的routing key發送。路由關鍵字中的第一個單詞將描述速度,第二個顏色和第三個種類:“..”。
我們創建了三個綁定:Q1綁定了綁定鍵“* .orange.”,Q2綁定了“.*.rabbit”和“lazy.#”。
Q1匹配所有的橙色動物。
Q2匹配關於兔子以及懶惰動物的消息。
- 練習,生產者發送如下消息,會進入那個隊列:
-
quick.orange.rabbit -> Q1 Q2
-
lazy.orange.elephant -> Q1 Q2
-
quick.orange.fox -> Q1
-
lazy.pink.rabbit -> Q2
-
quick.brown.fox -> 不匹配任意隊列,被丟棄
-
quick.orange.male.rabbit -> 不匹配任意隊列,被丟棄 (*只能匹配一個,且僅一個)
-
orange -> 不匹配任意隊列,被丟棄
4.1 生產者
使用topic類型的Exchange,發送消息的routing key有3種: item.isnert
、item.update
、item.delete
:
public class Send {
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 獲取通道
Channel channel = connection.createChannel();
// 聲明exchange,指定類型爲topic
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 消息內容
String message = "新增商品 : id = 1001";
// 發送消息,並且指定routing key 爲:insert ,代表新增
channel.basicPublish(EXCHANGE_NAME, "item.insert", null, message.getBytes());
System.out.println(" [事項服務:] Sent '" + message + "'");
channel.close();
connection.close();
}
}
4.2 消費者1
假設消費者1只接收兩種類型的消息:更新事項和刪除事項
public class Recv {
private final static String QUEUE_NAME = "topic_exchange_queue_1";
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 獲取通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 綁定隊列到交換機,同時指定需要訂閱的routing key。需要 update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.update");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.delete");
// 定義隊列的消費者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 獲取消息,並且處理,這個方法類似事件監聽,如果有消息的時候,會被自動調用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息體
String msg = new String(body);
System.out.println(" [消費者1] received : " + msg + "!");
}
};
// 監聽隊列,自動ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
4.3 消費者2
/**
* 消費者2
*/
public class Recv2 {
private final static String QUEUE_NAME = "topic_exchange_queue_2";
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 獲取到連接
Connection connection = ConnectionUtil.getConnection();
// 獲取通道
Channel channel = connection.createChannel();
// 聲明隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 綁定隊列到交換機,同時指定需要訂閱的routing key。訂閱 insert、update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.*");
// 定義隊列的消費者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 獲取消息,並且處理,這個方法類似事件監聽,如果有消息的時候,會被自動調用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息體
String msg = new String(body);
System.out.println(" [消費者2] received : " + msg + "!");
}
};
// 監聽隊列,自動ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}