訊息佇列(MQ)

Xin發表於2019-07-13

什麼是訊息佇列

訊息佇列,即MQ,Message Queue。

訊息佇列是典型的:生產者、消費者模型。生產者不斷向訊息佇列中生產訊息,消費者不斷的從佇列中獲取訊息。因為訊息的生產和消費都是非同步的,而且只關心訊息的傳送和接收,沒有業務邏輯的侵入,這樣就實現了生產者和消費者的解耦。

結合前面所說的問題:

  • 商品服務對商品增刪改以後,無需去操作索引庫或靜態頁面,只是傳送一條訊息,也不關心訊息被誰接收。

  • 搜尋服務和靜態頁面服務接收訊息,分別去處理索引庫和靜態頁面。

如果以後有其它系統也依賴商品服務的資料,同樣監聽訊息即可,商品服務無需任何程式碼修改。

 

AMQP和JMS

MQ是訊息通訊的模型,併發具體實現。現在實現MQ的有兩種主流方式:AMQP、JMS。

 

兩者間的區別和聯絡:

  • JMS是定義了統一的介面,來對訊息操作進行統一;AMQP是通過規定協議來統一資料互動的格式

  • JMS限定了必須使用Java語言;AMQP只是協議,不規定實現方式,因此是跨語言的。

  • JMS規定了兩種訊息模型;而AMQP的訊息模型更加豐富

常見MQ產品

 

 

  • ActiveMQ:基於JMS, Apache

  • RabbitMQ:基於AMQP協議,erlang語言開發,穩定性好

  • RocketMQ:基於JMS,阿里巴巴產品,目前交由Apache基金會

  • Kafka:分散式訊息系統,高吞吐量

RabbitMQ

RabbitMQ是基於AMQP的一款訊息管理系統

官網: http://www.rabbitmq.com/

官方教程:http://www.rabbitmq.com/getstarted.html

 

RabbitMQ基於Erlang語言開發:

下載和安裝

下載

官網下載地址:http://www.rabbitmq.com/download.html

 

 

我們使用的是:3.4.1版本

安裝

 


五種訊息模型

RabbitMQ提供了6種訊息模型,但是第6種其實是RPC,並不是MQ,因此不予學習。那麼也就剩下5種。

但是其實3、4、5這三種都屬於訂閱模型,只不過進行路由的方式不同。

 

 

匯入demo工程

我們通過一個demo工程來了解下RabbitMQ的工作方式:

 

 

基本訊息模型

說明

官方文件說明:

RabbitMQ是一個訊息的代理者(Message Broker):它接收訊息並且傳遞訊息。

你可以認為它是一個郵局:當你投遞郵件到一個郵箱,你很肯定郵遞員會終究會將郵件遞交給你的收件人。與此類似,RabbitMQ 可以是一個郵箱、郵局、同時還有郵遞員。

不同之處在於:RabbitMQ不是傳遞紙質郵件,而是二進位制的資料

基本訊息模型圖:

 

在上圖的模型中,有以下概念:

  • P:生產者,也就是要傳送訊息的程式

  • C:消費者:訊息的接受者,會一直等待訊息到來。

  • queue:訊息佇列,圖中紅色部分。類似一個郵箱,可以快取訊息;生產者向其中投遞訊息,消費者從其中取出訊息。

生產者

連線工具類:

public class ConnectionUtil {
    /**
     * 建立與RabbitMQ的連線
     * @return
     * @throws Exception
     */
    public static Connection getConnection() throws Exception {
        //定義連線工廠
        ConnectionFactory factory = new ConnectionFactory();
        //設定服務地址
        factory.setHost("192.168.56.101");
        //埠
        factory.setPort(5672);
        //設定賬號資訊,使用者名稱、密碼、vhost
        factory.setVirtualHost("/leyou");
        factory.setUsername("leyou");
        factory.setPassword("leyou");
        // 通過工程獲取連線
        Connection connection = factory.newConnection();
        return connection;
    }
}

  

生產者傳送訊息:

public class Send {

    private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] argv) throws Exception {
        // 獲取到連線
        Connection connection = ConnectionUtil.getConnection();
        // 從連線中建立通道,使用通道才能完成訊息相關的操作
        Channel channel = connection.createChannel();
        // 宣告(建立)佇列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 訊息內容
        String message = "Hello World!";
        // 向指定的佇列中傳送訊息
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        
        System.out.println(" [x] Sent '" + message + "'");

        //關閉通道和連線
        channel.close();
        connection.close();
    }
}

  

控制檯:

 

web控制檯檢視訊息

進入佇列頁面,可以看到新建了一個佇列:simple_queue

 

 

點選佇列名稱,進入詳情頁,可以檢視訊息:

 

在控制檯檢視訊息並不會將訊息消費,所以訊息還在。

消費者獲取訊息

public class Recv {
    private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] argv) throws Exception {
        // 獲取到連線
        Connection connection = ConnectionUtil.getConnection();
        // 建立通道
        Channel channel = connection.createChannel();
        // 宣告佇列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 定義佇列的消費者
        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(" [x] received : " + msg + "!");
            }
        };
        // 監聽佇列,第二個引數:是否自動進行訊息確認。
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

  

控制檯:

 

這個時候,佇列中的訊息就沒了:

 

消費者的訊息確認機制

通過剛才的案例可以看出,訊息一旦被消費者接收,佇列中的訊息就會被刪除。

那麼問題來了:RabbitMQ怎麼知道訊息被接收了呢?

這就要通過訊息確認機制(Acknowlege)來實現了。當消費者獲取訊息後,會向RabbitMQ傳送回執ACK,告知訊息已經被接收。不過這種回執ACK分兩種情況:

  • 自動ACK:訊息一旦被接收,消費者自動傳送ACK

  • 手動ACK:訊息接收後,不會傳送ACK,需要手動呼叫

大家覺得哪種更好呢?

這需要看訊息的重要性:

  • 如果訊息不太重要,丟失也沒有影響,那麼自動ACK會比較方便

  • 如果訊息非常重要,不容丟失。那麼最好在消費完成後手動ACK,否則接收訊息後就自動ACK,RabbitMQ就會把訊息從佇列中刪除。如果此時消費者當機,那麼訊息就丟失了。

我們之前的測試都是自動ACK的,如果要手動ACK,需要改動我們的程式碼:

public class Recv2 {
    private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] argv) throws Exception {
        // 獲取到連線
        Connection connection = ConnectionUtil.getConnection();
        // 建立通道
        final Channel channel = connection.createChannel();
        // 宣告佇列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 定義佇列的消費者
        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(" [x] received : " + msg + "!");
                // 手動進行ACK
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 監聽佇列,第二個引數false,手動進行ACK
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

  

注意到最後一行程式碼:

channel.basicConsume(QUEUE_NAME, false, consumer);
 

如果第二個引數為true,則會自動進行ACK;如果為false,則需要手動ACK。方法的宣告:

 

 

work訊息模型

說明

在剛才的基本模型中,一個生產者,一個消費者,生產的訊息直接被消費者消費。比較簡單。

Work queues,也被稱為(Task queues),任務模型。

當訊息處理比較耗時的時候,可能生產訊息的速度會遠遠大於訊息的消費速度。長此以往,訊息就會堆積越來越多,無法及時處理。此時就可以使用work 模型:讓多個消費者繫結到一個佇列,共同消費佇列中的訊息。佇列中的訊息一旦消費,就會消失,因此任務是不會被重複執行的。

 

角色:

  • P:生產者:任務的釋出者

  • C1:消費者,領取任務並且完成任務,假設完成速度較慢

  • C2:消費者2:領取任務並完成任務,假設完成速度快

生產者

生產者與案例1中的幾乎一樣:

public class Send {
    private final static String QUEUE_NAME = "test_work_queue";

    public static void main(String[] argv) throws Exception {
        // 獲取到連線
        Connection connection = ConnectionUtil.getConnection();
        // 獲取通道
        Channel channel = connection.createChannel();
        // 宣告佇列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 迴圈釋出任務
        for (int i = 0; i < 50; i++) {
            // 訊息內容
            String message = "task .. " + i;
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");

            Thread.sleep(i * 2);
        }
        // 關閉通道和連線
        channel.close();
        connection.close();
    }
}

  

不過這裡我們是迴圈傳送50條訊息。

消費者1

 

消費者2

 

 

與消費者1基本類似,就是沒有設定消費耗時時間。

這裡是模擬有些消費者快,有些比較慢。

 

接下來,兩個消費者一同啟動,然後傳送50條訊息:

 

 

可以發現,兩個消費者各自消費了25條訊息,而且各不相同,這就實現了任務的分發。

 

能者多勞

剛才的實現有問題嗎?

  • 消費者1比消費者2的效率要低,一次任務的耗時較長

  • 然而兩人最終消費的訊息數量是一樣的

  • 消費者2大量時間處於空閒狀態,消費者1一直忙碌

現在的狀態屬於是把任務平均分配,正確的做法應該是消費越快的人,消費的越多。

怎麼實現呢?

我們可以修改設定,讓消費者同一時間只接收一條訊息,這樣處理完成之前,就不會接收更多訊息,就可以讓處理快的人,接收更多訊息 :

 

 

再次測試:

 

訂閱模型分類

訂閱模型示意圖:

 

前面2個案例中,只有3個角色:

  • P:生產者,也就是要傳送訊息的程式

  • C:消費者:訊息的接受者,會一直等待訊息到來。

  • queue:訊息佇列,圖中紅色部分。類似一個郵箱,可以快取訊息;生產者向其中投遞訊息,消費者從其中取出訊息。

而在訂閱模型中,多了一個exchange角色,而且過程略有變化:

  • P:生產者,也就是要傳送訊息的程式,但是不再傳送到佇列中,而是發給X(交換機)

  • C:消費者,訊息的接受者,會一直等待訊息到來。

  • Queue:訊息佇列,接收訊息、快取訊息。

  • Exchange:交換機,圖中的X。一方面,接收生產者傳送的訊息。另一方面,知道如何處理訊息,例如遞交給某個特別佇列、遞交給所有佇列、或是將訊息丟棄。到底如何操作,取決於Exchange的型別。Exchange有以下3種型別:

    • Fanout:廣播,將訊息交給所有繫結到交換機的佇列

    • Direct:定向,把訊息交給符合指定routing key 的佇列

    • Topic:萬用字元,把訊息交給符合routing pattern(路由模式) 的佇列

Exchange(交換機)只負責轉發訊息,不具備儲存訊息的能力,因此如果沒有任何佇列與Exchange繫結,或者沒有符合路由規則的佇列,那麼訊息會丟失!

 

訂閱模型-Fanout

Fanout,也稱為廣播。

流程說明

流程圖:

 

在廣播模式下,訊息傳送流程是這樣的:

  • 1) 可以有多個消費者

  • 2) 每個消費者有自己的queue(佇列)

  • 3) 每個佇列都要繫結到Exchange(交換機)

  • 4) 生產者傳送的訊息,只能傳送到交換機,交換機來決定要發給哪個佇列,生產者無法決定。

  • 5) 交換機把訊息傳送給繫結過的所有佇列

  • 6) 佇列的消費者都能拿到訊息。實現一條訊息被多個消費者消費

生產者

兩個變化:

  • 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();
    }
}

  

消費者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

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);
    }
}

  

測試

我們執行兩個消費者,然後傳送1條訊息:

 

 

訂閱模型-Direct

說明

在Fanout模式中,一條訊息,會被所有訂閱的佇列都消費。但是,在某些場景下,我們希望不同的訊息被不同的佇列消費。這時就要用到Direct型別的Exchange。

在Direct模型下:

  • 佇列與交換機的繫結,不能是任意繫結了,而是要指定一個RoutingKey(路由key)

  • 訊息的傳送方在 向 Exchange傳送訊息時,也必須指定訊息的 RoutingKey

  • Exchange不再把訊息交給每一個繫結的佇列,而是根據訊息的Routing Key進行判斷,只有佇列的Routingkey與訊息的 Routing key完全一致,才會接收到訊息

流程圖:

 

圖解:

  • P:生產者,向Exchange傳送訊息,傳送訊息時,會指定一個routing key。

  • X:Exchange(交換機),接收生產者的訊息,然後把訊息遞交給 與routing key完全匹配的佇列

  • C1:消費者,其所在佇列指定了需要routing key 為 error 的訊息

  • C2:消費者,其所在佇列指定了需要routing key 為 info、error、warning 的訊息

生產者

此處我們模擬商品的增刪改,傳送訊息的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();
    }
}

  

消費者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);
    }
}

  

消費者2

我們此處假設消費者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);
    }
}

  

測試

我們分別傳送增、刪、改的RoutingKey,發現結果:

 

 

訂閱模型-Topic

說明

Topic型別的ExchangeDirect相比,都是可以根據RoutingKey把訊息路由到不同的佇列。只不過Topic型別Exchange可以讓佇列在繫結Routing key 的時候使用萬用字元!

 

Routingkey 一般都是有一個或多個單片語成,多個單詞之間以”.”分割,例如: item.insert

萬用字元規則:

#:匹配一個或多個詞

*:匹配不多不少恰好1個詞

 

舉例:

audit.#:能夠匹配audit.irs.corporate 或者 audit.irs

audit.*:只能匹配audit.irs

圖示:

 

解釋:

  • 紅色Queue:繫結的是usa.# ,因此凡是以 usa.開頭的routing key 都會被匹配到

  • 黃色Queue:繫結的是#.news ,因此凡是以 .news結尾的 routing key 都會被匹配

生產者

使用topic型別的Exchange,傳送訊息的routing key有3種: item.isnertitem.updateitem.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();
    }
}

  

消費者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);
    }
}

  

消費者2

我們此處假設消費者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);
    }
}

  

 

相關文章