RabbitMQ的佇列模式你真的懂嗎

公众号-JavaEdge發表於2024-09-11

0 前言

官網描述六類工作佇列模式:

  1. 簡單佇列模式:最簡單的工作佇列,一個訊息生產者,一個訊息消費者,一個佇列。另稱點對點模式
  2. 工作模式:一個訊息生產者,一個交換器,一個訊息佇列,多個消費者。也稱點對點模式
  3. 釋出/訂閱模式:無選擇接收訊息,一個訊息生產者,一個交換器,多個訊息佇列,多個消費者
  4. 路由模式:基於釋出/訂閱模式,有選擇的接收訊息,即透過 routing 路由進行匹配條件是否滿足接收訊息
  5. 主題模式:同樣是在釋出/訂閱模式的基礎上,根據主題匹配進行篩選是否接收訊息,比第四類更靈活
  6. RPC模式:擁有請求/回覆的。也就是有響應的,這是其它都沒的

1 簡單佇列模式

1 實現功能

一個生產者 P 傳送訊息到佇列 Q,一個消費者 C 接收:

Pro

Pro負責建立訊息佇列,併傳送訊息入列:

  1. 獲取連線
  2. 建立通道
  3. 建立佇列宣告
  4. 傳送訊息
  5. 關閉佇列
public class Producer {

    private static final String QUEUE_NAME = "test_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection newConnection = MQConnectionUtils.newConnection();
        Channel channel = newConnection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        String msg = "我是生產者生成的訊息";
        System.out.println("生產者傳送訊息:" + msg);
        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
        channel.close();
        newConnection.close();
    }
}

Con

  1. 獲取連線
  2. 獲取通道
  3. 監聽佇列
public class Customer {

    private static final String QUEUE_NAME = "test_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        System.out.println("002");
        Connection newConnection = MQConnectionUtils.newConnection();
        Channel channel = newConnection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                String msgString = new String(body, "UTF-8");
                System.out.println("消費者獲取訊息:" + msgString);
            }
        };
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
    }
}

建立vhost

2 工作佇列模式

將耗時的任務分發給多個消費者(工作者)。

主要解決:處理資源密集型任務,且還要等他完成。有了工作佇列,就可將具體的工作放到後面去做,將工作封裝為一個訊息,傳送到佇列中,一個工作程序就可取出訊息並完成工作。若啟動了多個工作程序,則工作就可在多個程序間共享。

工作佇列也稱公平性佇列模式,迴圈分發,若有兩個消費者,預設RabbitMQ按序將每條訊息發給下一個 Con,每個消費者獲得相同數量的訊息,即輪詢。

Pro

建立50個訊息

public class Producer2 {

    private static final String QUEUE_NAME = "test_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection newConnection = MQConnectionUtils.newConnection();
        Channel channel = newConnection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
         /**保證一次只分發一次 限制傳送給同一個消費者 不得超過一條訊息 */
        channel.basicQos(1);
        for (int i = 1; i <= 50; i++) {
            String msg = "生產者訊息_" + i;
            System.out.println("生產者傳送訊息:" + msg);
            channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
        }
        channel.close();
        newConnection.close();
    }
}

Con

public class Customer2_1 {

    private static final String QUEUE_NAME = "test_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        System.out.println("001");
        Connection newConnection = MQConnectionUtils.newConnection();
        final Channel channel = newConnection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        /** 保證一次只分發一次 限制傳送給同一個消費者 不得超過一條訊息 */
        channel.basicQos(1);
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                String msgString = new String(body, "UTF-8");
                System.out.println("消費者獲取訊息:" + msgString);
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                } finally {
                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            }
        };
        channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
    }
}

迴圈分發

啟動生產者

啟動兩個消費者

Pro傳送了50條訊息進入佇列,而上方消費者啟動圖裡很明顯的看到輪詢的效果,就是每個消費者會分到相同的佇列任務。

公平分發

由於上方模擬的是非常簡單的訊息佇列的消費,假如有一些非常耗時的任務,某個消費者在緩慢地進行處理,而另一個消費者則空閒,顯然是非常消耗資源的。如一個1年的程式設計師,跟一個3年的程式設計師,分配相同的任務量,明顯3年的程式設計師處理起來更加得心應手,很快就無所事事了,但是3年的程式設計師拿著非常高的薪資!顯然3年的程式設計師應該承擔更多的責任,咋辦?

發生上述問題的原因是 RabbitMQ 收到訊息後就立即分發出去,而沒有確認各個工作者未返回確認的訊息數量,類似UDP,面向無連線。可用 basicQos,並將引數 prefetchCount 設為1,告訴 RabbitMQ 我每次值處理一條訊息,你要等我處理完了再分給我下一個。這樣 RabbitMQ 就不會輪流分發了,而是尋找空閒的工作者進行分發。

final Channel channel = newConnection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/** 保證一次只分發一次 限制傳送給同一個消費者 不得超過一條訊息 */
channel.basicQos(1);

訊息持久化

背景

上邊我們提到的公平分發是由消費者收取訊息時確認解決的,但是這裡面又會出現被 kill 的情況。

當有多個消費者同時收取訊息,且每個消費者在接收訊息的同時,還要處理其它的事情,且會消耗很長的時間。在此過程中可能會出現一些意外,比如訊息接收到一半的時候,一個消費者死掉了。

這種情況要使用訊息接收確認機制,可以執行上次當機的消費者沒有完成的事情。

但是在預設情況下,我們程式建立的訊息佇列以及存放在佇列裡面的訊息,都是非持久化的。當RabbitMQ死掉了或者重啟了,上次建立的佇列、訊息都不會儲存。咋辦?

引數配置

引數配置一:生產者建立佇列宣告時,修改第二個引數為 true

/**3.建立佇列宣告 */
channel.queueDeclare(QUEUE_NAME, true, false, false, null);

引數配置二:生產者傳送訊息時,修改第三個引數為MessageProperties.PERSISTENT_TEXT_PLAIN

for (int i = 1; i <= 50; i++) {
    String msg = "生產者訊息_" + i;
    System.out.println("生產者傳送訊息:" + msg);
    channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
}

小結

  • 迴圈分發:消費者端在通道上開啟訊息應答機制,並確保能返回接收訊息的確認資訊,這樣可以保證消費者發生故障也不會丟失訊息
  • 訊息持久化:伺服器端和客戶端都要指定佇列的持久化和訊息的持久化,這樣可以保證RabbitMQ重啟,佇列和訊息也不會丟失
  • 公平分發:指定消費者接收的訊息個數,避免出現訊息均勻推送出現的資源不合理利用的問題

3 釋出訂閱模式

工作佇列模式是直接在生產者與消費者裡宣告好一個佇列,訊息就只會對應同型別的消費者。這種只處理同種型別的訊息有弊端。

3.1 案例

入口網站,使用者註冊完後一般都會傳送訊息通知使用者註冊結果。如在一個系統中,使用者註冊資訊有郵箱、手機號,在註冊完後會向郵箱和手機號都傳送註冊完成資訊。

利用 MQ 實現業務非同步處理,若用工作佇列,就宣告一個註冊資訊佇列。註冊完成後生產者向佇列提交一條註冊資料,消費者取出資料同時向郵箱以及手機號傳送兩條訊息。但實際上郵箱和手機號資訊傳送實際上是不同的業務邏輯,不應放在一塊處理。

這時就可利用釋出/訂閱模式將訊息傳送到轉換機(EXCHANGE),宣告兩個不同的佇列(郵箱、手機),並繫結到交換機。這樣生產者只需要釋出一次訊息,兩個佇列都會接收到訊息發給對應的消費者:

只需簡單的將佇列繫結到交換機。一個傳送到交換機的訊息都會被轉發到與該交換機繫結的所有佇列。就像子網廣播,每臺子網內的主機都獲得一份複製的訊息。

3.2 啥是釋出訂閱模式

可將訊息傳送給不同型別的消費者。即釋出一次,消費多個:

X表示交換機、紅色表示佇列。

展示郵件、簡訊的例子,透過繫結到一個交換機,但是

3.3 實戰

public class ProducerFanout {

    private static final String EXCHANGE_NAME = "fanout_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        /** 1.建立新的連線 */
        Connection connection = MQConnectionUtils.newConnection();
        /** 2.建立通道 */
        Channel channel = connection.createChannel();
        /** 3.繫結的交換機 引數1互動機名稱 引數2 exchange型別 */
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        /** 4.傳送訊息 */
        for (int i = 0; i < 10; i++)
        {
            String message = "使用者註冊訊息:" + i;
            System.out.println("[send]:" + message);
          	// 第二個引數為空類似於表示全域性廣播,只要繫結到該佇列上的消費者理論上是都可收到
            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("utf-8"));
            try {
                Thread.sleep(5 * i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        /** 5.關閉通道、連線 */
        channel.close();
        connection.close();
        /** 注意:如果消費沒有繫結交換機和佇列,則訊息會丟失 */
    }
}

郵件消費者

public class ConsumerEmailFanout {

    private static final String QUEUE_NAME = "consumerFanout_email";
    private static final String EXCHANGE_NAME = "fanout_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        System.out.println("郵件消費者啟動");
        /* 1.建立新的連線 */
        Connection connection = MQConnectionUtils.newConnection();
        /* 2.建立通道 */
        Channel channel = connection.createChannel();
        /* 3.消費者關聯佇列 */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        /* 4.消費者繫結交換機 引數1 佇列 引數2交換機 引數3 routingKey */
      	// 第三個引數置為空時,可以接收到生產者所有的訊息(生產者 routingKey 引數為空時)
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("消費者獲取生產者訊息:" + msg);
            }
        };
        /* 5.消費者監聽佇列訊息 */
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

簡訊消費者

public class ConsumerSMSFanout {

    private static final String QUEUE_NAME = "ConsumerFanout_sms";
    private static final String EXCHANGE_NAME = "fanout_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        System.out.println("簡訊消費者啟動");
        /* 1.建立新的連線 */
        Connection connection = MQConnectionUtils.newConnection();
        /* 2.建立通道 */
        Channel channel = connection.createChannel();
        /* 3.消費者關聯佇列 */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        /* 4.消費者繫結交換機 引數1 佇列 引數2交換機 引數3 routingKey */
      	// 第三個引數置為空時,可接收到生產者所有的訊息(生產者 routingKey 引數為空時)
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("消費者獲取生產者訊息:" + msg);
            }
        };
        /* 5.消費者監聽佇列訊息 */
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

執行

先執行兩個con,再執行pro。如沒有提前將佇列繫結到交換機,直接執行pro,訊息是不會發到任何佇列裡的。

生產者

簡訊消費者

郵件消費者

小結

相比工作模式,釋出訂閱模式引入了交換機,型別上更靈活。

pro不是直接操作佇列,而是將資料發給交換機,由交換機將資料發給與之繫結的佇列。從不加特定引數的執行結果中可以看到,兩種型別的消費者(email,sms)都收到相同數量訊息。

必須宣告交換機,並設定模式:channel.exchangeDeclare(EXCHANGE_NAME, "fanout"),fanout 指分發模式(將每一條訊息都傳送到與交換機繫結的佇列)

佇列必須繫結交換機:channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

生產者傳送訊息到交換機,多個消費者宣告多個佇列,與交換機進行繫結,佇列中的訊息可以被所有消費者消費,類似QQ群訊息

4 路由模式

就是釋出訂閱模式(Publish/Subscribe Pattern)中的直連交換機(Direct Exchange)。一種基於路由鍵(Routing Key)來路由訊息的模式。在這種模式下,生產者傳送訊息時會指定一個路由鍵,交換機會根據這個路由鍵將訊息路由到與之匹配的佇列。

Pro

使用 channel.basicPublish 方法傳送訊息,並指定交換機名稱和路由鍵。交換機會根據路由鍵將訊息路由到與之匹配的佇列。

Con

在消費者程式碼中,我們宣告瞭一個直接交換機(direct 型別),並繫結了一個佇列。在繫結佇列時,我們使用 channel.queueBind 方法,並指定交換機名稱、佇列名稱和路由鍵。交換機會根據路由鍵將訊息路由到與之匹配的佇列。

特點

  • 路由鍵匹配:訊息的路由鍵必須與佇列繫結的路由鍵完全匹配,才能將訊息路由到該佇列。
  • 直接交換機:直接交換機根據路由鍵進行精確匹配,適用於需要精確控制訊息路由的場景。

透過這種方式,路由模式可以實現基於路由鍵的精確訊息路由,適用於需要將訊息傳送到特定佇列的場景。

5 主題模式

屬於釋出訂閱模式的TopicExchange(主題交換機)。Queue 透過 routing key 繫結到 TopicExchange,當訊息到達TopicExchange後,TopicEkchange 根據訊息的 routing key 將訊息路由到一個或者多個Queue。

關注我,緊跟本系列專欄文章,咱們下篇再續!

作者簡介:魔都架構師,多家大廠後端一線研發經驗,在分散式系統設計、資料平臺架構和AI應用開發等領域都有豐富實踐經驗。

各大技術社群頭部專家博主。具有豐富的引領團隊經驗,深厚業務架構和解決方案的積累。

負責:

  • 中央/分銷預訂系統效能最佳化
  • 活動&券等營銷中臺建設
  • 交易平臺及資料中臺等架構和開發設計
  • 車聯網核心平臺-物聯網連線平臺、大資料平臺架構設計及最佳化
  • LLM Agent應用開發
  • 區塊鏈應用開發
  • 大資料開發挖掘經驗
  • 推薦系統專案

目前主攻市級軟體專案設計、構建服務全社會的應用系統。

參考:

  • 程式設計嚴選網

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章