不依賴 Spring,你會如何自實現 RabbitMQ 訊息的消費(一)2Q

ocenwimtaegrad發表於2024-11-22

開心一刻

上午一好哥們微信我哥們:哥們在幹嘛,晚上出來吃飯我:就我倆嗎哥們:對啊我:那多沒意思,我叫倆女的出來哥們:好啊,哈哈哈晚上吃完飯到家後,我給哥們發訊息我:今天吃的真開心,下次繼續哥們:開心尼瑪呢!下次再叫你老婆和你女兒來,我特麼踢死你

開心一刻

寫在前面

正文開始之前了,我們先來正確審視下文章標題:不依賴 Spring,你會如何自實現 RabbitMQ 訊息的消費,主要從兩點來進行審視

  1. 不依賴 Spring

作為一個 Javaer,關於 Spring 的重要性,我相信你們都非常清楚;回頭看看你們開發的專案,是不是都是基於 Spring 的?如果不依賴 Spring,你們還能繼續開發嗎?不過話說回來,既然 Spring 能帶來諸多便利,該用還得用,不要頭鐵,不要造低效輪子!

如果能造出比 Spring 優秀的輪子,那你應該造!

你們可能會說:不依賴 Spring 就不依賴嘛,我可以依賴 Spring Boot 噻;你們要是這麼聊天,那就沒法聊了

這還咋聊
Spring Boot 是不是基於 Spring 的?沒有 Spring,Spring Boot 也是跑不起來的;不依賴 Spring 的言外之意就是不依賴 Spring 生態,當然也包括 Spring Boot

關於 不依賴 Spring,我就當你們審視清楚了哦
2. 依賴 RabbitMQ Java Client

與 RabbitMQ 服務端的互動,咱們就不要逞強去自實現了,老實點用官方提供的 Java Client 就好

<dependency>
    <groupId>com.rabbitmqgroupId>
    <artifactId>amqp-clientartifactId>
    <version>5.7.3version>
dependency>

注意 client 版本要與 RabbitMQ 版本相容

所以文章標題就可以轉換成

只依賴 RabbitMQ Java Client,不依賴 Spring,如何自實現 RabbitMQ 訊息的消費

另外,我再帶你們回顧下 RabbitMQ 的 Connection 和 Channel

  1. Connection

Connection 是客戶端與 RabbitMQ 伺服器之間的一個 TCP 連線,它是進行通訊的基礎,允許客戶端傳送命令到 RabbitMQ 伺服器並接收響應;Connection 是比較重的資源,不能隨意建立與關閉,一般會以 的方式進行管理。每個 Connection 可以包含多個 Channel
2. Channel

Channel多路複用 連線(Connection)中的一條獨立的雙向資料流通道。客戶端與 RabbitMQ 服務端之間的大多數操作都是在 Channel 上進行的,而不是在 Connection 上直接進行。Channel 比 Connection 更輕量級,可以在同一連線中建立多個 Channel 以實現併發處理

Channel 與 Consumer 之間的關係是一對多的,具體來說,一個 Channel 可以繫結多個 Consumer,但每個 Consumer 只能繫結到一個

自實現

我們採取主幹到枝葉的實現方式,逐步實現並完善 RabbitMQ 訊息的消費

主流程

依賴 RabbitMQ Java Client 來消費 RabbitMQ 的訊息,程式碼實現非常簡單,網上一搜一大把

/**
 * @author: 青石路
 */
public class RabbitTest1 {

    private static final String QUEUE_NAME = "qsl.queue";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = initConnectionFactory();
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.basicConsume(QUEUE_NAME, false, new QslConsumer(channel));
        System.out.println(Thread.currentThread().getName() + " 執行緒執行完畢,消費者:" + consumerTag + "已經就緒");
    }

    public static ConnectionFactory initConnectionFactory() {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("10.5.108.226");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("admin");
        factory.setVirtualHost("/");
        factory.setConnectionTimeout(30000);
        return factory;
    }

    static class QslConsumer extends DefaultConsumer {

        QslConsumer(Channel channel) {
            super(channel);
        }

        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            String message = new String(body, StandardCharsets.UTF_8);
            System.out.println(Thread.currentThread().getName()+ " 收到訊息:" + message);
            this.getChannel().basicAck(envelope.getDeliveryTag(), false);
        }
    }
}

是不是很簡單?

這裡我得補充下,exchangequeue 沒有在程式碼中宣告,繫結關係也沒有宣告,是為了簡化程式碼,因為文章標題是 消費;實際 exchangequeuebinding 這些已經存在,如下圖所示

exchange_queue宣告

上述程式碼,我相信你們都能看懂,主要強調下 2 點

  1. 訊息是否自動 Ack

對應程式碼

channel.basicConsume(QUEUE_NAME, false, new QslConsumer(channel));

basicConsume 的第二個引數,其註釋如下

basicConsume_訊息確認方式
autoAcktrue 表示訊息在送達到 Consumer 後被 RabbitMQ 服務端確認,訊息就會從佇列中剔除了;autoAckfalse 表示 Consumer 需要顯式的向 RabbitMQ 服務端進行訊息確認

因為我們將 autoAck 設定成了 true,所以 main 執行緒存活的時間內,5 個訊息被送達到 main 執行緒後就被 RabbitMQ 服務端確認了,也就從佇列中刪除了
2. 手動確認

如果 Consumer 的 autoAck 設定的是 false,那麼需要顯示的進行訊息確認

this.getChannel().basicAck(envelope.getDeliveryTag(), false);

否則 RabbitMQ 服務端會將訊息一直保留在佇列中,反覆投遞

執行 main 方法,控制檯輸出如下

消費者就緒
我們去 RabbitMQ 控制檯看下佇列 qsl.queue 的消費者

RabbitMQ消費者
Consumer tag 值是:amq.ctag-PxjqYiujeCvyYlgtvMz9EQ,與控制檯的輸出一致;我們手動往佇列中傳送一條訊息

傳送訊息
控制檯輸出如下

手動傳送一條訊息_控制檯輸出
自此,主流程就通了,此時已經實現 RabbitMQ 訊息的消費

多消費者

單消費者肯定存在效能瓶頸,所以我們需要支援多消費者,並且是同個佇列的多消費者;實現方式也很簡單,只需要調整下 main 方法即可

public static void main(String[] args) throws Exception {
    ConnectionFactory factory = initConnectionFactory();
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
    QslConsumer qslConsumer = new QslConsumer(channel);
    String consumerTag1 = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
    String consumerTag2 = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
    String consumerTag3 = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
    System.out.println(Thread.currentThread().getName() + " 執行緒執行完畢,消費者["
            + Arrays.asList(consumerTag1, consumerTag2, consumerTag3) + "]已經就緒");
}

執行main 方法後 Channel 與 Consumer 關係如下

同個Channel_3個Consumer
此時是同個 Channel 繫結了 3 個不同的 Consumer;當然也可以一對一繫結,main 方法調整如下

public static void main(String[] args) throws Exception {
    ConnectionFactory factory = initConnectionFactory();
    Connection connection = factory.newConnection();
    Channel channel1 = connection.createChannel();
    Channel channel2 = connection.createChannel();
    Channel channel3 = connection.createChannel();
    QslConsumer qslConsumer1 = new QslConsumer(channel1);
    QslConsumer qslConsumer2 = new QslConsumer(channel2);
    QslConsumer qslConsumer3 = new QslConsumer(channel3);
    String consumerTag1 = channel1.basicConsume(QUEUE_NAME, false, qslConsumer1);
    String consumerTag2 = channel2.basicConsume(QUEUE_NAME, false, qslConsumer2);
    String consumerTag3 = channel3.basicConsume(QUEUE_NAME, false, qslConsumer3);
    System.out.println(Thread.currentThread().getName() + " 執行緒執行完畢,消費者["
            + Arrays.asList(consumerTag1, consumerTag2, consumerTag3) + "]已經就緒");
}

執行 main 方法後 Channel 與 Consumer 關係如下

3個Channel_3個Consumer
既然兩種方式都可以實現多消費者,哪那種方式更好呢

Channel 與 Consumer 一對一繫結更好!

Channel 之間是執行緒安全的,同個 Channel 內非執行緒安全,所以同個 Channel 上同時處理多個消費者存在併發問題;另外 RabbitMQ 的訊息確認機制是基於Channel 的,如果一個 Channel 上繫結多個消費者,那麼訊息確認會變得複雜,非常容易導致訊息重複消費或丟失

也許你們會覺得 一對一 的繫結相較於 一對多 的繫結,存在資源浪費問題;確實是有這個問題,但我們要知道,Channel 是 Connection 中的一條獨立的雙向資料流通道,非常輕量級,相較於併發帶來的一系列問題而言,這點小小的資源浪費可以忽略不計了

消費者數量能不能配置化呢,當然可以,調整非常簡單

private static final int concurrency = 3;

public static void main(String[] args) throws Exception {
    ConnectionFactory factory = initConnectionFactory();
    Connection connection = factory.newConnection();
    for (int i = 0; i < concurrency; i++) {
        Channel channel = connection.createChannel();
        QslConsumer qslConsumer = new QslConsumer(channel);
        String consumerTag = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
        System.out.println("消費者:" + consumerTag + " 已經就緒");
    }
}

concurrency 的值是從資料庫讀取,還是從配置檔案中獲取,就可以發揮你們的想象呢;如果依賴 Spring 的話,往往會用配置檔案的方式注入進來

消費者預取數

佇列 qsl.queue 沒有消費者的情況下,我們往佇列中新增 5 條訊息:我是訊息1 ~ 我是訊息5,然後調整下 handleDelivery

@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    String message = new String(body, StandardCharsets.UTF_8);
    System.out.println(consumerTag + " 收到訊息:" + message);
    this.getChannel().basicAck(envelope.getDeliveryTag(), false);
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

最後執行 main,控制檯輸出如下

5條訊息同個消費者消費
大家注意看框住的那部分,5 條訊息被同一個消費者給消費了!5 條訊息為什麼不是負載均衡到 3 個消費者呢?這是因為消費者的 prefetch count(即 預取數)沒有設定

prefetch count 是消費者在接收訊息時,告訴 RabbitMQ 一次最多可以傳送多少條訊息給該消費者。預設情況下,這個值是 0,這意味著 RabbitMQ 會盡可能快地將訊息分發給消費者,而不考慮消費者當前的處理能力

再回過頭去看控制檯的輸出,是不是就能理解了?一旦某個消費者就緒,佇列中的 5 條訊息全部推給它了,後面就緒的 2 個消費者就沒有訊息可消費了;所以我們需要配置 prefetch count 以實現負載均衡,調整很簡單

private static final String QUEUE_NAME = "qsl.queue";
private static final int concurrency = 3;
private static final int prefetchCount = 1;

public static void main(String[] args) throws Exception {
    ConnectionFactory factory = initConnectionFactory();
    Connection connection = factory.newConnection();
    for (int i = 0; i < concurrency; i++) {
        Channel channel = connection.createChannel();
        channel.basicQos(prefetchCount);
        QslConsumer qslConsumer = new QslConsumer(channel);
        String consumerTag = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
        System.out.println("消費者:" + consumerTag + " 已經就緒");
    }
}

然後重複如上的測試,控制檯輸出如下

負載均衡消費
是不是實現了我們想要的 負載均衡

prefetch count 的設定需要根據實際的業務需求和消費者的處理能力進行調整;如果設定得太高,可能會導致記憶體佔用過多;如果設定得太低,則可能無法充分利用消費者的處理能力

其他完善

限於篇幅,我就只列舉幾個還待完善的點

  1. 目前只支援單個佇列,需要支援多個佇列
  2. 目錄消費邏輯單一固定,需要支援動態指定邏輯,不同的佇列對應不同的消費邏輯
  3. 消費者支援停止和重啟
  4. ...

關於這些點,我們下篇不見不散

總結

  1. Connection、Channel、Consumer 之間的關係需要理清楚

Connection 是 TCP 連線;Channel 是 Connection 中的雙向資料流通道;Channel 可以繫結多個 Consumer,但推薦一個 Channel 只繫結一個 Consumer

IO 多路複用 是網路程式設計中常用的技術,建議大家掌握
2. 基於 RabbitMQ Java Client 提供的 API,實現了訊息消費、多消費者以及負載均衡

沒有 Spring,我們照樣可以很優雅的消費 RabbitMQ 的訊息

本部落格參考楚門加速器p。轉載請註明出處!

相關文章