RabbitMQ非官方教程(四)發佈和訂閱

上一節我們創建了一個工作隊列,並且假設每個任務都恰好交付給一個消費者。在本章節中,我們將消息傳達給多個消費者,這種模式稱爲“發佈/訂閱”。

爲了說明這種模式,我們將構建一個簡單的日誌記錄系統。它由兩個程序組成:第一個程序將發出日誌消息,第二個程序將接收並打印它們。在我們的日誌系統中,接收器程序的每個運行副本都將獲得消息。這樣我們將能夠運行一個接收器並將日誌定向到磁盤。同時我們將能夠運行另一個接收器並在屏幕上查看日誌。實際上已發佈的日誌消息將被廣播到所有接收者。

交換器

前面教程中,我們都是向隊列發送消息,然後從隊列接收消息。現在是添加一個新概念——交換器(交換機)。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得到下面結果,通過交換機把消息發送到怪怪的隊列中,然後消費者把隊列中消息拿出來之後隊列自動刪除。刷新一下網頁之後怪怪的隊列自動被刪除不存在了。

後面教程再介紹其他三中交換機的匹配規則。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章