上一節我們創建了一個工作隊列,並且假設每個任務都恰好交付給一個消費者。在本章節中,我們將消息傳達給多個消費者,這種模式稱爲“發佈/訂閱”。
爲了說明這種模式,我們將構建一個簡單的日誌記錄系統。它由兩個程序組成:第一個程序將發出日誌消息,第二個程序將接收並打印它們。在我們的日誌系統中,接收器程序的每個運行副本都將獲得消息。這樣我們將能夠運行一個接收器並將日誌定向到磁盤。同時我們將能夠運行另一個接收器並在屏幕上查看日誌。實際上已發佈的日誌消息將被廣播到所有接收者。
交換器
前面教程中,我們都是向隊列發送消息,然後從隊列接收消息。現在是添加一個新概念——交換器(交換機)。RabbitMQ消息傳遞模型中的核心思想是:生產者不能直接把生產的消息發送給隊列。實際應用中生產者根本不知道是否將消息傳遞到那個隊列中,生產者只能將消息發送到交換器中。交流是一件非常簡單的事情,一方面它接收來自生產者的消息;另一方面將它們推入隊列。
交換器必須確切知道如何處理收到的消息,是否應將其附加到特定隊列?是否應該將其附加到許多隊列中?還是應該丟棄它。這些操作都是由交換類型定義決定 。
交換器的類型, 內置的有四種, 分別是:
- fanout
- direct
- topic
- headers
這是本節的demo代碼地址:https://gitee.com/mjTree/javaDevelop/tree/master/testDemo
先不着急寫Demo代碼,看一下這麼定義的。首先是fanout類型的交換器,在代碼中定義交換器:
channel.exchangeDeclare("logs", "fanout");
fanout類型的交換比較簡單,它只是將接收到的所有消息廣播到它知道的所有隊列中,而這正是我們記錄器所需要的。
/*
這是NewTask.java中消息的定義,第一個參數是交換的名稱。
我們填寫的是空字符串,它表示默認或無名稱交換
*/
channel.basicPublish("", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("UTF-8"));
// 現在我們把它修改爲,這樣生產者就不會直接把消息交給隊列
channel.basicPublish( "logs", "",
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("UTF-8"));
當我們要在生產者和消費者之間共享隊列時,給隊列命名很重要。而且在交換器把消息按上面四種規則發佈時也需要隊列名的規整性。但這不是我們的記錄器的情況,我們希望聽到所有日誌消息,而不是一部分。我們只對當前正在發送的消息感興趣,對舊消息不感興趣。爲了解決這個問題,我們需要兩件事。
首先,無論何時連接到Rabbit,我們都需要一個全新的空隊列。因此我們需要創建一個具有隨機名稱的隊列,或者甚至更好的讓服務器爲我們選擇一個隨機隊列名稱。其次一旦我們斷開了使用者的連接時隊列將被自動刪除。在Java客戶端中,當我們不向queueDeclare()提供任何參數時,我們會使用生成的名字來創建一個非持久的、排他的、自動刪除的隊列:
String queueName = channel.queueDeclare().getQueue();
非持久化前面教程已經說到了,排他性是指僅由一個連接使用並且該連接關閉時隊列將被刪除。
排他隊列
排他隊列只能通過其聲明連接來使用(消耗,清除,刪除等)。嘗試使用來自其他連接的互斥隊列將導致通道級異常 RESOURCE_LOCKED,並顯示一條錯誤消息,提示 無法獲得對鎖定隊列的互斥訪問。
排他隊列的聲明連接已關閉或消失時(例如,由於基礎TCP連接丟失),它們將被刪除。因此,它們僅適用於客戶端特定的瞬態。
通常,以服務器名稱命名排他隊列。
我們上面生成的queueName是一個隨機隊列名稱,可能看起來像amq.gen-vfAboU8KeitFZq5UYpnZKQ。現在我們已經創建了一個叫logs的交換機和一個叫queueName的消息隊列。此時我們需要告訴交換機將消息發送到我們的隊列,而交換和隊列之間的關係稱爲綁定。
通過下面代碼,我們把交換機logs和消息隊列queueName綁定在一起。在我們發送消息的時候,如果第一個參數提供的交換機名稱是logs,我們只會把消息轉發到queueName隊列中,其他隊列收不到。這就是fanout類型交換機的特性。
channel.queueBind(queueName, "logs", "");
舉個例子:
// 這裏我們定義了一個交換機和三個消息隊列
exchangeDeclare(exchangeName="First", type="fanout")
queueDeclare(queue= "A")
queueDeclare(queue= "B")
queueDeclare(queue= "C")
// 開始綁定
queueBind(exchange="First", queue="A")
queueBind(exchange="First", queue="B")
// 循環發送10次消息
basicPublish(exchange="First", body="Hello MJ")
結果:A和B隊列各有10個消息,C隊列沒有
接下來寫代碼,新建Third包,建立EmitLog.java和ReceiveLogs.java。
package com.mytest.rabbitMQ.Third;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class EmitLog {
// 定義交換器名稱
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 創建fanout類型的交換機
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String message = argv.length < 1 ? "info:Hello World!":
String.join(" ", argv);
channel.basicPublish(EXCHANGE_NAME, "",
null,
message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
package com.mytest.rabbitMQ.Third;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class ReceiveLogs {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 創建fanout類型的交換機
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 創建一個非持久的、排他的、自動刪除的隊列
String queueName = channel.queueDeclare().getQueue();
System.out.println("自動生成的隊列名稱:" + queueName);
//channel.queueDeclare(TASK_QUEUE_NAME, durable, false, false, null); // 之前教程創建的持久化隊列
// 交換機和隊列綁定
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag ->{});
}
}
這裏我們先執行EmitLog,然後去網頁查看是否生成該隊列,是生成一個名字怪怪的隊列。然後我們執行ReceiveLogs得到下面結果,通過交換機把消息發送到怪怪的隊列中,然後消費者把隊列中消息拿出來之後隊列自動刪除。刷新一下網頁之後怪怪的隊列自動被刪除不存在了。
後面教程再介紹其他三中交換機的匹配規則。