RabbitMQ 佇列使用基礎教程

授客發表於2024-09-01

實踐環境

JDK 1.8.0_121

amqp-client 5.16.0

附:檢視不同版本的amqp-client客戶端支援的Java JDK版本

https://www.rabbitmq.com/client-libraries/java-versions

mavn settings.xml

<?xml version="1.0" encoding="UTF-8" ?>
<settings xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd" xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <localRepository>D:\maven-repo</localRepository>
    <mirrors>
        <mirror>
            <id>aliyunmaven</id>
            <name>阿里雲公共倉庫</name>
            <url>https://maven.aliyun.com/repository/public</url>
            <mirrorOf>*</mirrorOf>
        </mirror>
    </mirrors>
    <profiles>
        <profile>
            <repositories>                
            </repositories>
            <pluginRepositories>
            </pluginRepositories>
            <id>artifactory</id>
        </profile>
        <profile>
            <id>jdk-1.8</id>
            <activation>
                <activeByDefault>true</activeByDefault>
                <jdk>1.8</jdk>
            </activation>
            <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <maven.compiler.source>1.8</maven.compiler.source>
                <maven.compiler.target>1.8</maven.compiler.target>
                <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
            </properties>
        </profile>
    </profiles>
    <activeProfiles>
        <activeProfile>artifactory</activeProfile>
    </activeProfiles>
</settings>

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>ore.example</groupId>
    <artifactId>rabbitMQStudy</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>3.5.4</maven.compiler.source>
        <maven.compiler.target>3.5.4</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <amqp.client.version>5.16.0</amqp.client.version>
        <slf4j.api.version>1.7.36</slf4j.api.version>
    </properties>
    <dependencies>
        <!-- 新增 RabbitMQ 客戶端依賴 -->
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>${amqp.client.version}</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.api.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>${slf4j.api.version}</version>
        </dependency>
    </dependencies>
</project>

Hello World

場景:生產者 -> hello佇列 -> 消費者

Sent.java

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;

import java.nio.charset.StandardCharsets;

public class Sent {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128"); // 設定RabbitMQ伺服器
        factory.setPort(5672);  // 預設埠為 5672
        factory.setUsername("testacc"); // 設定連線登入使用者
        factory.setPassword("test1234"); // 設定使用者訪問密碼
        factory.setAutomaticRecoveryEnabled(true);  // 開啟Connection自動恢復功能,這意味著如果連線丟失,客戶端將嘗試重新連線到 RabbitMQ 伺服器。
        factory.setNetworkRecoveryInterval(5000); // 嘗試重連時間間隔 // 設定為 5000:如果RabbitMQ客戶端失去連線後,每5秒自動嘗試重連一次
        factory.setVirtualHost("/"); // 設定虛擬主機,預設 /
        factory.setConnectionTimeout(30 * 1000); // 設定TCP連線超時時間 預設 60000(60秒)
        factory.setHandshakeTimeout(30 * 1000); // 設定SSL握手超時時間 預設 10000(10秒)
        factory.setShutdownTimeout(0); // 設定客戶端關閉前等待操作完成的最大時間 預設 10000(10秒)

        // 因為Connection和Channel都實現了java.lang.AutoCloseable,使用try-with-resources語句,可以在程式碼中顯示關閉連線和通道
        try (Connection connection = factory.newConnection(); // 建立連線
             Channel channel = connection.createChannel()) { // 建立通道
            channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 宣告一個佇列(等冪操作),如果佇列不存在,自動建立,Routing Key: hello 交換機:(AMQP default)
            String message = "Hello World!";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }

}

說明:

basicPublish函式說明

void basicPublish(String var1, String var2, BasicProperties var3, byte[] var4) throws IOException;
void basicPublish(String var1, String var2, boolean var3, BasicProperties var4, byte[] var5) throws IOException;
void basicPublish(String var1, String var2, boolean var3, boolean var4, BasicProperties var5, byte[] var6) throws IOException;
  • var1 : 指定要傳送的交換機的名稱, 如果設定為空字串, 那麼訊息會被髮送到RabbitMQ的預設交換機.
  • var2 : 路由鍵, 用於指定訊息要路由到的佇列
  • var3:是否強制路由,如果設定為true,並且訊息無法路由到任何佇列(沒有匹配的繫結),那麼RabbitMQ會返回一個錯誤給生產者。如果設定為false,則訊息將被丟棄
  • var4:是否立即釋出(immediate flag)。如果設定為true,並且訊息無法路由到任何消費者(沒有匹配的佇列或消費者不線上),那麼RabbitMQ會返回一個錯誤給生產者。如果設定為false,訊息將被儲存在佇列中等待消費者。
  • BasicProperties 可以使用PERSISTENT_TEXT_PLAIN表示傳送的是需要持久化的訊息,其實也就是將BasicProperties中的deliveryMode設定為2
  • props : 訊息的屬性, 這是一個可選引數, 裡面有: 訊息型別, 格式, 優先順序, 過期時間等等
  • body : 訊息體, 也就是要傳送的訊息本身
  • var5 : 訊息屬性,同props
  • var6:訊息體,同body

queueDeclare函式說明

com.rabbitmq.client.AMQP.Queue.DeclareOk queueDeclare() throws IOException;
com.rabbitmq.client.AMQP.Queue.DeclareOk (String var1, boolean var2, boolean var3, boolean var4, Map<String, Object> var5) throws IOException;

說明:

  • 當呼叫第一個不帶引數的queueDeclare()時,RabbitMQ 會自動建立一個新的佇列,該佇列的名稱將由 RabbitMQ 自動生成,並且這個佇列是非持久的、排他的、自動刪除的,且不帶任何額外的引數。

    由於沒有指定佇列名稱,你通常無法預先知道佇列的確切名稱,這可能會在某些場景下造成不便,比如當你需要多個消費者共享同一個佇列時。此外,由於佇列是非持久的,如果 RabbitMQ 伺服器重啟,這個佇列將會丟失,所有在佇列中的訊息也會丟失。

    該方法適用於那些不需要複雜佇列配置的場景,比如臨時測試或簡單應用,可能不適用於需要持久化儲存或明確指定佇列名稱的場景。

  • 第二個方法允許更細緻地配置佇列的屬性,引數說明如下:

    • var1:佇列的名稱,不能為空,且要求在 RabbitMQ 伺服器上是唯一的。
    • var2:是否持久化佇列。true -- 持久化,即 RabbitMQ 伺服器重啟後依然存在。false,非持久化的,伺服器重啟後佇列將不存在。
    • var3:是否排他。true--是,佇列只能被宣告它的連線使用,並且當連線關閉時,佇列會被自動刪除。這通常用於臨時佇列。false -- 否
    • var4:是否自動刪除。true,當最後一個消費者斷開連線後,佇列會自動刪除。如果設定為 false,則不會自動刪除佇列。
    • var5:一組額外的佇列引數,可以用來設定佇列的更多高階特性。例如,佇列的最大長度、訊息生存時間等。

    該方法適用於那些需要複雜佇列配置和高階特性的場景。

  • 當呼叫第二個方法時,RabbitMQ會檢查是否已經存在具有相同名稱的佇列,如果如果佇列不存在,則根據提供的引數建立一個新的佇列。如果已存在,則不再建立

basicConsume 函式說明

basicConsume 有20個過載函式,這裡就不一一列出了,常用方法如下:

String basicConsume(String var1, boolean var2, DeliverCallback var3, CancelCallback var4) throws IOException;

說明:

  • var1:消費者要從中接收訊息的佇列名稱

  • var2:設定是否自動確認訊息。

    true 自動確認訊息--一旦訊息被交付給消費者,RabbitMQ 會自動將其標記為已確認,訊息就從佇列中移除,即使消費者還沒有實際處理完這條訊息。這種模式下,如果消費者在處理訊息時崩潰或發生錯誤,那麼這條訊息就會丟失,因為 RabbitMQ 認為它已經被成功處理了。

    false 不啟動確認訊息。消費者需要顯式地呼叫 basicAck 方法來確認訊息已被成功處理。這樣,如果消費者在處理訊息時崩潰,RabbitMQ 會重新將這條訊息放回佇列中,等待其他消費者處理,從而保證了訊息的可靠性。

  • var3:一個回撥函式,當 RabbitMQ 向消費者傳送訊息,切消費被消費者成功訊息後,會自動呼叫這個回撥。開發者可以在該回撥函式中處理接收到的訊息,比如列印訊息內容或者進行其他業務邏輯。

  • var4:可選的回撥函式,當消費者取消訂閱時會自動呼叫這個回撥。這個回撥可以用於執行清理工作,比如釋放資源、記錄日誌等。

Recv.java

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.nio.charset.StandardCharsets;

public class Recv {

    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128"); // 設定RabbitMQ伺服器IP
        factory.setUsername("testacc"); // 設定連線登入使用者
        factory.setPassword("test1234"); // 設定使用者訪問密碼

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();  // 建立通道
        channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 宣告需要消費的佇列,如果佇列不存在則建立
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> { // 定義一個回撥函式用於緩衝伺服器推送的訊息
            String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { }); // 啟動一個消費者,並返回服務端生成的消費者標識

    }
}

說明:這裡為啥不用類似 生產者程式碼中的try-with-resources語句,因為這裡想讓消費者持續非同步監聽佇列訊息,而不是消費完一條訊息後馬上退出。

執行測試

先執行Recv,控制檯輸出:

 [*] Waiting for messages. To exit press CTRL+C

再執行Sent,控制檯輸出:

 [x] Sent 'Hello World!'

執行Recv的控制檯輸出:

 [x] Received 'Hello World!'

此外,執行Recv後, 檢視RabbitMQ管理介面,可以看到Channels Tab頁新增顯示一條通道,Connections Tab頁新增顯示一條連線,Queues介面新增一個名為hello的佇列

參考連結:

https://www.rabbitmq.com/tutorials/tutorial-one-java

工作佇列(任務佇列)

在本節中,將建立一個工作佇列,用於在多個woker之間分配耗時的任務。
工作佇列(又名:任務佇列)背後的主要思想是避免立即執行資源密集型任務,並等待其完成。而是把任務安排在以後完成。將任務封裝為訊息並將其傳送到佇列。在後臺執行的工作程序將pop出任務並最終執行作業。當你執行多個worker時,任務將在它們之間共享。
這個概念在web應用程式中特別有用,因為在短HTTP請求視窗內無法處理複雜的任務。

場景--輪詢(round-robin)

說明:P 代表生產者,Queue為佇列, C 代表 消費者

NewTask.java

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;

public class NewTask {
    private final static String TASK_QUEUE_NAME = "task_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setUsername("testacc");
        factory.setPassword("test1234");
        factory.setShutdownTimeout(0);


        try (Connection connection = factory.newConnection();
            Channel channel = connection.createChannel()) {
            channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

            String[] msgs = {"First message...", "Second message", "Third message...", "Fourth message", "Fifth message..."};
            for (int i = 0; i < msgs.length; i++) {
                channel.basicPublish("", TASK_QUEUE_NAME, // 第一個引數代表交換機名稱,設定為空,表示使用預設交換機
                        MessageProperties.PERSISTENT_TEXT_PLAIN,
                        msgs[i].getBytes("UTF-8"));

                System.out.println(" [x] Sent '" + msgs[i] + "'");
            }

        }
    }

}

Worker.java

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

public class Worker {

    private static final String TASK_QUEUE_NAME = "task_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setUsername("testacc");
        factory.setPassword("test1234");

        final Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();

        channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null); // 設定第2個引數為True,設定佇列為持久化佇列
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

//        channel.basicQos(1);

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");

            System.out.println(" [x] Received '" + message + "'");
            try {
                doWork(message);
            } finally {
                System.out.println(" [x] Done");
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
        };
        // 將第二個引數設定為false,即不自動應答,保證訊息處理的可靠性
        channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
    }

    private static void doWork(String task) {
        /*模擬執行任務耗時*/
        for (char ch : task.toCharArray()) {
            if (ch == '.') {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException _ignored) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}

先執行Worker,開啟兩個Worker程序,然後執行NewTask 五次,檢視控制檯輸出

NewTask執行輸出

 [x] Sent 'First message...'
 [x] Sent 'Second message'
 [x] Sent 'Third message...'
 [x] Sent 'Fourth message'
 [x] Sent 'Fifth message...'

第一Worker輸出

 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'First message...'
 [x] Done
 [x] Received 'Third message...'
 [x] Done
 [x] Received 'Fifth message...'
 [x] Done

第二個Worker輸出

 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'Second message'
 [x] Done
 [x] Received 'Fourth message'
 [x] Done

說明:

預設情況下,RabbitMQ將按順序將每條訊息傳送給下一個消費者。平均而言,每個消費者都會收到相同數量的訊息。這種分發訊息的方式稱為輪詢。

場景-公平分發(Fair dispatch)

你可能已經注意到,分發仍然沒有完全按照我們的要求工作。例如,在有兩個worker的情況下,當所有奇數訊息都很重(訊息處理比較耗時),偶數訊息都很輕時(訊息處理比較簡單,不怎麼耗時),一個worker會一直很忙,另一個幾乎不做任何工作。好吧,RabbitMQ對此一無所知,仍然會均勻地傳送訊息。
這是因為RabbitMQ只是在訊息進入佇列時分發訊息。它不考慮消費者未確認的訊息數量。它只是盲目地將每第n條訊息分派給第n個消費者

為了克服這一點,可使用帶引數prefetchCount = 1basicQos方法。這告訴RabbitMQ一次不要給一個worker傳送多條訊息。或者,換句話說,在處理完並確認前一條訊息之前,不要向worker傳送新訊息。取而代之,將訊息傳送給下一個不忙的worker

int prefetchCount = 1;
channel.basicQos(prefetchCount);

注意佇列大小
如果所有的worker都很忙,佇列可能會排滿。需要密切關注這一點,也許可以增加更多的worker,或者採取其他策略。

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

public class Worker {

    private static final String TASK_QUEUE_NAME = "task_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setUsername("testacc");
        factory.setPassword("test1234");

        final Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();

        channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null); // 設定第2個引數為True,設定佇列為持久化佇列
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        channel.basicQos(1);

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");

            System.out.println(" [x] Received '" + message + "'");
            try {
                doWork(message);
            } finally {
                System.out.println(" [x] Done");
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
        };
        // 將第二個引數設定為false,即不自動應答,保證訊息處理的可靠性
        channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
    }

    private static void doWork(String task) {
        /*模擬執行任務耗時*/
        for (char ch : task.toCharArray()) {
            if (ch == '.') {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException _ignored) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}

先執行Worker,開啟兩個Worker程序,然後執行NewTask,檢視控制檯輸出

第一Worker輸出

 [x] Received 'First message...'
 [x] Done
 [x] Received 'Fourth message'
 [x] Done

第二個Worker輸出

 [x] Received 'Second message'
 [x] Done
 [x] Received 'Third message...'
 [x] Done
 [x] Received 'Fifth message...'
 [x] Done

參考連結:

https://www.rabbitmq.com/tutorials/tutorial-two-java

釋出和訂閱

上節示例中,我們建立了一個工作佇列。工作佇列背後的假設是,每個任務只傳遞給一個工作者。本例將實現向多個消費者傳遞一個資訊。這種模式被稱為“釋出/訂閱”。

場景:

說明:P 代表生產者,X 代表 交換機,Q 代表佇列

EmitLog.java

import com.rabbitmq.client.BuiltinExchangeType;
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("192.168.88.128");
        factory.setPort(5672);
        factory.setUsername("testacc");
        factory.setPassword("test1234");

        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT); // 宣告名為 logs,型別為 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 + "'");
        }
    }
}

ReceiveLogs.java

import com.rabbitmq.client.*;

public class ReceiveLogs {
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setUsername("testacc");
        factory.setPassword("test1234");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        // 假設需求是:1,無論何時,消費者連線到RabbitMQ,都需要一個新的空的佇列
        // 2,端開消費者時在,自動刪除佇列
        String queueName = channel.queueDeclare().getQueue(); // channel.queueDeclare() 定義一個非持久,排他的,自動刪除的,名稱隨機生成且保持唯一的佇列
        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 -> { });
    }
}

先執行 ReceiveLogs(開兩個程序),再執行 EmitLog(不帶引數執行)

EmitLog 執行輸出:

 [x] Sent 'info: Hello World!'

ReceiveLogs執行控制檯輸出:

 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'info: Hello World!'

參考連結:

https://www.rabbitmq.com/tutorials/tutorial-three-java

路由

上節示例中,實現了多個消費者訂閱所有佇列訊息,本節示例中,將實現僅訂閱訊息子集,即訂閱部分訊息。

直接交換機(Direct exchange)

The routing algorithm behind a direct exchange is simple - a message goes to the queues whose binding key exactly matches the routing key of the message.

上一節示例中實現了將所有訊息廣播給所有消費者,本節希望在此基礎上,以允許根據訊息的嚴重性對其進行過濾,實現不同消費者接收不同級別的日誌

上節使用的扇出交換機(fanout),沒有太多的靈活性——它只能進行無意識的廣播。所以,本節示例中將使用直接交換機(direct)。直接交換機背後的路由演算法很簡單——佇列的繫結鍵和訊息的路由鍵完全匹配,則將訊息進入到該佇列。

為了說明這一點,假設有以下設定:

這裡,我們可以看到直接交換機X繫結了兩個佇列。第一個佇列用繫結鍵orange繫結,第二個佇列有兩個繫結,一個繫結鍵black,另一個繫結鍵 green
在這種設定下,使用orange 路由鍵釋出到交換機的訊息將被路由到佇列Q1。使用路由鍵為blackgreen的釋出的訊息件將路由到Q2。所有其他訊息都將被丟棄。

多個繫結

使用相同的繫結鍵繫結多個佇列是完全合法的。以下示例中,使用繫結鍵blackXQ1之間新增繫結。在這種情況下,direct交換機將表現得像fanout交換機,將訊息廣播到所有匹配的佇列。擁有路由鍵為black的訊息將同時傳送到Q1Q2

本節示例中實現的場景:

EmitLogDirect.java

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class EmitLogDirect {

    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setUsername("testacc");
        factory.setPassword("test1234");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

            String severity = getSeverity(argv);
            String message = getMessage(argv);

            channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + severity + "':'" + message + "'");
        }
    }

    private static String getSeverity(String[] strings) {
        if (strings.length < 1)
            return "info";
        return strings[0];
    }

    private static String getMessage(String[] strings) {
        if (strings.length < 2)
            return "Hello World!";
        return joinStrings(strings, " ", 1);
    }

    private static String joinStrings(String[] strings, String delimiter, int startIndex) {
        int length = strings.length;
        if (length == 0) return "";
        if (length <= startIndex) return "";
        StringBuilder words = new StringBuilder(strings[startIndex]);
        for (int i = startIndex + 1; i < length; i++) {
            words.append(delimiter).append(strings[i]);
        }
        return words.toString();
    }
}

ReceiveLogsDirect.java

import com.rabbitmq.client.*;

public class ReceiveLogsDirect {

    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setUsername("testacc");
        factory.setPassword("test1234");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        String queueName = channel.queueDeclare().getQueue();

        if (argv.length < 1) {
            System.err.println("Usage: ReceiveLogsDirect [info] [warning] [error]");
            System.exit(1);
        }

        for (String severity : argv) {
            channel.queueBind(queueName, EXCHANGE_NAME, severity); // 第三個引數為繫結鍵
        }
        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 '" + delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
}

函式說明:

com.rabbitmq.client.AMQP.Queue.BindOk queueBind(String var1, String var2, String var3) throws IOException;
com.rabbitmq.client.AMQP.Queue.BindOk queueBind(String var1, String var2, String var3, Map<String, Object> var4) throws IOException;

說明:

  • var1:佇列名稱

  • var2: 交換機名稱

  • var3:用於繫結交換機和佇列的路由鍵,為了同basic_publish routingKey引數混淆,稱之為 繫結建(bindingKey)

  • var4:一些額外引數

先執行ReceiveLogsDirect(開兩個程序,分別攜帶info warning errorwarning error引數執行),再執行EmitLogDirect,檢視控制檯輸出。

第一次執行EmitLogDirect時攜帶以下引數

error "Run. Run. Or it will explode."

執行ReceiveLogsDirect的兩個控制檯都輸出以下內容

 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'error':'Run. Run. Or it will explode.'

第2次,去掉執行引數,直接執行執行EmitLogDirect,結果僅帶info warning error引數執行ReceiveLogsDirect的控制檯增加輸出以下內容:

 [x] Received 'info':'Hello World!'

參考連結:

https://www.rabbitmq.com/tutorials/tutorial-four-java

主題

上節示例中,採用了direct交換機,實現了選擇性接收訊息,雖然有所改進,單仍然有侷限性,不能基於多個標準進行路由,比如紀要根據日誌嚴重級別來訂閱日誌,同時還要根據日誌訊息產生源訂閱日誌,為此還需要了解更復雜的主題交換機。

主題交換機(Topic exchange)

傳送到主題交換機的訊息不能是任意的路由鍵——它必須是一個由點分隔的單詞列表。單詞可以是任意的,通常是與訊息相關的一些特徵。幾個有效的路由鍵示例:"stock.usd.nyse"、“nyse.vmw”、“quick.ornge.rabbit”。如你喜歡,路由鍵可以包含任意多個單詞,但是最多不能超過255個位元組。

繫結鍵也必須採用相同的形式。主題交換機背後的邏輯類似於直接交換機——使用特定路由鍵傳送的訊息將被傳遞到使用匹配繫結鍵繫結的所有佇列。但是,對繫結鍵來說,有兩個重要的特殊情況:

  1. * 可以匹配一個單詞。
  2. # 開匹配0個或更多個單次。

用一個例子來解釋這一點:

在這個例子中,我們將傳送描述動物的訊息。訊息將使用由三個單詞(兩點)組成的路由鍵進行傳送。路由鍵中的第一個單詞將描述速度,第二個單詞描述顏色,第三個單詞描述物種:“..”。

我們建立了三個繫結:Q1用繫結鍵“*.ornge.*”繫結,Q2用“*.*.rabit”和“lazy.#”繫結。

這些繫結可以概括為:

  1. Q1對所有橙色的動物都感興趣。
  2. Q2想聽聽關於兔子的一切,以及關於懶惰動物的一切。

訊息路由示例:

  • 帶有路由鍵“quick.ornge.robit”、 "lazy.orange.elephant"的訊息將會被髮送給所有佇列。

  • 帶有路由鍵“quick.orange.fox”的訊息將僅被投放入Q1佇列中。

  • 帶有路由鍵"lazy.pink.rabbit"的訊息的將僅被投放入Q2佇列中,且只會放入一次,雖然匹配兩個繫結鍵。

  • 帶有路由鍵"quick.brown.fox orangequick.orange.new.rabbit的訊息將不會被投放入任何佇列中,會被丟棄。

  • 帶有路由鍵lazy.orange.new.rabbit訊息將被投放入Q2佇列中

說明:
主題交換j機器功能強大,可以像其他交換一樣執行。

  • 當佇列使用“#” 繫結鍵繫結時,它將接收所有訊息,而不管路由鍵如何,就像fanout交換機一樣。
  • 當繫結中不使用特殊字元“*”和“#”時,主題交換機的行為就像direct交換機一樣。

EmitLogTopic

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class EmitLogTopic {

    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setUsername("testacc");
        factory.setPassword("test1234");

        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {

            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

            String routingKey = getRouting(argv);
            String message = getMessage(argv);

            channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");
        }
    }

    private static String getRouting(String[] strings) {
        if (strings.length < 1)
            return "anonymous.info";
        return strings[0];
    }

    private static String getMessage(String[] strings) {
        if (strings.length < 2)
            return "Hello World!";
        return joinStrings(strings, " ", 1);
    }

    private static String joinStrings(String[] strings, String delimiter, int startIndex) {
        int length = strings.length;
        if (length == 0) return "";
        if (length < startIndex) return "";
        StringBuilder words = new StringBuilder(strings[startIndex]);
        for (int i = startIndex + 1; i < length; i++) {
            words.append(delimiter).append(strings[i]);
        }
        return words.toString();
    }
}

ReceiveLogsTopic

import com.rabbitmq.client.*;

public class ReceiveLogsTopic {

    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.88.128");
        factory.setPort(5672);
        factory.setUsername("testacc");
        factory.setPassword("test1234");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        String queueName = channel.queueDeclare().getQueue();

        if (argv.length < 1) {
            System.err.println("Usage: ReceiveLogsTopic [binding_key]...");
            System.exit(1);
        }

        for (String bindingKey : argv) {
            channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
        }

        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 '" + delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
    }
}

分別使用以下引數先執行ReceiveLogsTopic(開4個程序),再執行EmitLogTopic(帶引數"kern.critical" "A critical kernel error"執行),檢視控制檯輸出。

"#"
"kern.*"
"*.critical"
"kern.*" "*.critical"

結果,執行ReceiveLogsTopic的四個控制檯輸出:

 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'kern.critical':'A critical kernel error'

執行EmitLogTopic的控制檯輸出:

 [x] Sent 'kern.critical':'A critical kernel error'

參考連結:https://www.rabbitmq.com/tutorials/tutorial-five-java

參考連結

https://github.com/rabbitmq/rabbitmq-tutorials/tree/main/java

相關文章