沒用過訊息佇列?一文帶你體驗RabbitMQ收發訊息

和耳朵發表於2020-08-03

人生終將是場單人旅途,孤獨之前是迷茫,孤獨過後是成長。

楔子

先給大家說聲抱歉,最近一週都沒有發文,有一些比較要緊重要的事需要處理。

今天正好得空,本來說準備寫SpringIOC相關的東西,但是發現想要梳理一遍還是需要很多時間,所以我打算慢慢寫,先把MQ給寫了,再慢慢寫其他相關的,畢竟偏理論的東西一遍要比較難寫,像MQ這種偏實戰的大家可以clone程式碼去玩一玩,還是比較方便的。

同時MQ也是Java進階不必可少的技術棧之一,所以Java開發從業者對它是必須要了解的。

現在市面上有三種訊息佇列比較火分別是:RabbitMQRocketMQKafka

今天要講的訊息佇列中我會以RabbitMQ作為案例來入門,因為SpringBoot的amqp中預設只整合了RabbitMQ,用它來講會方便許多,且RabbitMQ的效能和穩定性都很不錯,是一款經過時間考驗的開源元件。

祝有好收穫。

本文程式碼: 碼雲地址GitHub地址

1. ?訊息佇列?

訊息佇列(MQ)全稱為Message Queue,是一種應用程式對應用程式的通訊方法。

翻譯一下就是:在應用之間放一個訊息元件,然後應用雙方通過這個訊息元件進行通訊。

好端端的為啥要在中間放個元件呢?

小系統其實是用不到訊息佇列的,一般分散式系統才會引入訊息佇列,因為分散式系統需要抗住高併發,需要多系統解耦,更需要對使用者比較友好的響應速度,而訊息佇列的特性可以天然解耦,方便非同步更能起到一個頂住高併發的削峰作用,完美解決上面的三個問題。


然萬物抱陽負陰,系統之間突然加了箇中介軟體,提高系統複雜度的同時也增加了很多問題:

  • 訊息丟失怎麼辦?
  • 訊息重複消費怎麼辦?
  • 某些任務需要訊息的順序訊息,順序消費怎麼保證?
  • 訊息佇列元件的可用性如何保證?

這些都是使用訊息佇列過程中需要思考需要考慮的地方,訊息佇列能給你帶來很大的便利,也能給你帶來一些對應的麻煩。

上面說了訊息佇列帶來的好處以及問題,而這些不在我們今天這篇的討論範圍之內,我打算之後再寫這些,我們今天要做的是搭建出一個訊息佇列環境,讓大家感受一下基礎的發訊息與消費訊息,更高階的問題會放在以後討論。

2. ?RabbitMQ一覽

RabbitMQ是一個訊息元件,是一個erlang開發的AMQP(Advanced Message Queue)的開源實現。

AMQP,即Advanced Message Queuing Protocol,一個提供統一訊息服務的應用層標準高階訊息佇列協議,是應用層協議的一個開放標準,為面向訊息的中介軟體設計。

RabbitMQ採用了AMQP協議,至於這協議怎麼怎麼樣,我們關心的是RabbitMQ結構如何且怎麼用。

還是那句話,學東西需要先觀其大貌,我們要用RabbitMQ首先要知道它整體是怎麼樣,這樣才有利於我們接下來的學習。

我們先來看看我剛畫的架構圖,因為RabbitMQ實現了AMQP協議,所以這些概念也是AMQP中共有的。

rabbit架構圖

  • Broker: 中介軟體本身。接收和分發訊息的應用,這裡指的就是RabbitMQ Server。

  • Virtual host: 虛擬主機。出於多租戶和安全因素設計的,把AMQP的基本元件劃分到一個虛擬的分組中,類似於網路中的namespace概念。當多個不同的使用者使用同一個RabbitMQ server提供的服務時,可以劃分出多個vhost,每個使用者在自己的vhost建立exchange/queue等。

  • Connection: 連線。publisher/consumer和broker之間的TCP連線。斷開連線的操作只會在client端進行,Broker不會斷開連線,除非出現網路故障或broker服務出現問題。

  • Channel: 渠道。如果每一次訪問RabbitMQ都建立一個Connection,在訊息量大的時候建立TCP Connection的開銷會比較大且效率也較低。Channel是在connection內部建立的邏輯連線,如果應用程式支援多執行緒,通常每個thread建立單獨的channel進行通訊,AMQP method包含了channel id幫助客戶端和message broker識別channel,所以channel之間是完全隔離的。Channel作為輕量級的Connection極大減少了作業系統建立TCP connection的開銷。

  • Exchange: 路由。根據分發規則,匹配查詢表中的routing key,分發訊息到queue中去。

  • Queue: 訊息的佇列。訊息最終被送到這裡等待消費,一個message可以被同時拷貝到多個queue中。

  • Binding: 繫結。exchange和queue之間的虛擬連線,binding中可以包含routing key。Binding資訊被儲存到exchange中的查詢表中,用於message的分發依據。


看完了這些概念,我再給大家梳理一遍其流程:

當我們的生產者端往Broker(RabbitMQ)中傳送了一條訊息,Broker會根據其訊息的標識送往不同的Virtual host,然後Exchange會根據訊息的路由key和交換器型別將訊息分發到自己所屬的Queue中去。

然後消費者端會通過Connection中的Channel獲取剛剛推送的訊息,拉取訊息進行消費。

Tip:某個Exchange有哪些屬於自己的Queue,是由Binding繫結關係決定的。

3. ?RabbitMQ環境

上面講了RabbitMQ大概的結構圖和一個訊息的執行流程,講完了理論,這裡我們就準備實操一下吧,先進行RabbitMQ安裝。

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

由於我還沒有屬於自己MAC電腦,所以這裡的演示就按照Windows的來了,不過大家都是程式設計師,安裝個東西總歸是難不倒大家的吧?

Windows下載地址:https://www.rabbitmq.com/install-windows.html

進去之後可以直接找到Direct Downloads,下載相關EXE程式進行安裝就可以了。

由於RabbitMQ是由erlang語言編寫的,所以安裝之前我們還需要安裝erlang環境,你下載RabbitMQ之後直接點選安裝,如果沒有相關環境,安裝程式會提示你,然後會讓你的瀏覽器開啟erlang的下載頁面,在這個頁面上根據自己的系統型別點選下載安裝即可,安裝完畢後再去安裝RabbitMQ

這兩者的安裝都只需要一直NEXT下一步就可以了。

安裝完成之後可以按一下Windows鍵看到效果如下:

rabbitmq安裝效果

Tip:其中Rabbit-Command後面會用到,是RabbitMQ的命令列操作檯。


安裝完RabbitMQ我們需要對我們的開發環境也匯入RabbitMQ相關的JAR包。

為了方便起見,我們可以直接使用Spring-boot-start的方式匯入,這裡面也會包含所有我們需要用到的RabbitMQ相關的JAR包。

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
</dependencies>

直接引入spring-boot-starter-amqp即可。

4. ✍Hello World

搭建好環境之後,我們就可以上手了。

考慮到這是一個入門文章,讀者很多可能沒有接觸過RabbitMQ,直接使用自動配置的方式可能會令大家很迷惑,因為自動配置會遮蔽很多細節,導致大家只看到了被封裝後的樣子,不利於大家理解。

所以在本節Hello World這裡,我會直接使用最原始的連線方式就行演示,讓大家看到最原始的連線的樣子。

Tip:這種方式演示的程式碼我都在放在prototype包下面。

4.1 生產者

先來看看生產者程式碼,也就是我們push訊息的程式碼:

    public static final String QUEUE_NAME = "erduo";

    // 建立連線工廠
    ConnectionFactory connectionFactory = new ConnectionFactory();

    // 連線到本地server
    connectionFactory.setHost("127.0.0.1");

    // 通過連線工廠建立連線
    Connection connection = connectionFactory.newConnection();

    // 通過連線建立通道
    Channel channel = connection.createChannel();

    // 建立一個名為耳朵的佇列,該佇列非持久(RabbitMQ重啟後會消失)、非獨佔(非僅用於此連結)、非自動刪除(伺服器將不再使用的佇列刪除)
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);

    String msg = "hello, 我是耳朵。" + LocalDateTime.now().toString();
    // 釋出訊息
    // 四個引數為:指定路由器,指定key,指定引數,和二進位制資料內容
    channel.basicPublish("", QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));

    System.out.println("生產者傳送訊息結束,傳送內容為:" + msg);
    channel.close();
    connection.close();

程式碼我都給了註釋,但是我還是要給大家講解一遍,梳理一下。

先通過RabbitMQ中的ConnectionFactory配置一下將要連線的server-host,然後建立一個新連線,再通過此連線建立通道(Channel),通過這個通道建立佇列和傳送訊息。

這裡看上去還是很好理解的,我需要把建立佇列和傳送訊息這裡再拎出來說一下。

建立佇列

    AMQP.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments) throws IOException;

建立佇列的方法裡面有五個引數,第一個是引數是佇列的名稱,往後的三個引數代表不同的配置,最後一個引數是額外引數。

  • durable:代表是否將此佇列持久化。

  • exclusive:代表是否獨佔,如果設定為獨佔佇列則此佇列僅對首次宣告它的連線可見,並在連線斷開時自動刪除。

  • autoDelete:代表斷開連線後是否自動刪除此佇列。

  • arguments:代表其他額外引數。

這些引數中durable經常會用到,它代表了我們可以對佇列做持久化,以保證RabbitMQ當機恢復後此佇列也可以自行恢復。

傳送訊息

    void basicPublish(String exchange, String routingKey, AMQP.BasicProperties props, byte[] body) throws IOException;

傳送訊息的方法裡是四個引數,第一個是必須的指定exchange,上面的示例程式碼中我們傳入了一個空字串,這代表我們交由預設的匿名exchange去幫我們路由訊息。

第二個引數是路由key,exchange會根據此key對訊息進行路由轉發,第三個引數是額外引數,講訊息持久化時會用到一下,最後一個引數就是我們要傳送的資料了,需要將我們的資料轉成位元組陣列的方式傳入。

測試

講完了這些API之後,我們可以測試一下我們的程式碼了,run一下之後,會在控制檯打出如下:

生產者測試結果01

這樣之後我們就把訊息傳送到了RabbitMQ中去,此時可以開啟RabbitMQ控制檯(前文安裝時提到過)去使用命令rabbitmqctl.bat list_queues去檢視訊息佇列現在的情況:

檢視佇列狀態

可以看到有一條message在裡面,這就代表我們的訊息已經傳送成功了,接下來我們可以編寫一個消費者對裡面的message進行消費了。

4.2 消費者

消費者程式碼和生產者的差不多,都需要建立連線建立通道:

    // 建立連線工廠
    ConnectionFactory connectionFactory = new ConnectionFactory();

    // 連線到本地server
    connectionFactory.setHost("127.0.0.1");

    // 通過連線工廠建立連線
    Connection connection = connectionFactory.newConnection();

    // 通過連線建立通道
    Channel channel = connection.createChannel();

    // 建立消費者,阻塞接收訊息
    com.rabbitmq.client.Consumer consumer = new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            System.out.println("-------------------------------------------");
            System.out.println("consumerTag : " + consumerTag);
            System.out.println("exchangeName : " + envelope.getExchange());
            System.out.println("routingKey : " + envelope.getRoutingKey());
            String msg = new String(body, StandardCharsets.UTF_8);
            System.out.println("訊息內容 : " + msg);
        }
    };

    // 啟動消費者消費指定佇列
    channel.basicConsume(Producer.QUEUE_NAME, consumer);
//        channel.close();
//        connection.close();

建立完通道之後,我們需要建立一個消費者物件,然後用這個消費者物件去消費指定佇列中的訊息。

這個示例中我們就是新建了一個consumer,然後用它去消費佇列-erduo中的訊息。

最後兩句程式碼我給註釋掉了,因為一旦把連線也關閉了,那我們的消費者就不能保持消費狀態了,所以要開著連線,監聽此佇列。

ok,執行這段程式,然後我們的消費者會去佇列-erduo拿到裡面的訊息,效果如下:

消費者test01

  • consumerTag:是這個訊息的標識。

  • exchangeName:是這個訊息所傳送exchange的名字,我們先前傳入的是空字串,所以這裡也是空字串。

  • exchangeName:是這個訊息所傳送路由key。

這樣我們的程式就處在一個監聽的狀態下,你再次呼叫生產者傳送訊息消費者就會實時的在控制上列印訊息內容。

5. ?訊息接收確認(ACK)

上面我們演示了生產者和消費者,我們生產者傳送一條訊息,消費者消費一條資訊,這個時候我們的RabbitMQ應該有多少訊息?

理論上來說傳送一條,消費一條,現在裡面應該是0才對,但是現在的情況並不是:

檢視佇列狀態

訊息佇列裡面還是有1條資訊,我們重啟一下消費者,又列印了一遍我們消費過的那條訊息,通過訊息上面的時間我們可以看出來還是當時我們傳送的那條資訊,也就是說我們消費者消費過了之後這條資訊並沒有被刪除。

消費者test01

這種狀況出現的原因是因為RabbitMQ訊息接收確認機制,也就是說一條資訊被消費者接收到了之後,需要進行一次確認操作,這條訊息才會被刪除。

RabbitMQ中預設消費確認是手動的,也可以將其設定為自動刪除,自動刪除模式消費者接收到訊息之後就會自動刪除這條訊息,如果訊息處理過程中發生了異常,這條訊息就等於沒被處理完但是也被刪除掉了,所以這裡我們會一直使用手動確認模式。

訊息接受確認(ACK)的程式碼很簡單,只要在原來消費者的程式碼里加上一句就可以了:

    com.rabbitmq.client.Consumer consumer = new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            System.out.println("-------------------------------------------");
            System.out.println("consumerTag : " + consumerTag);
            System.out.println("exchangeName : " + envelope.getExchange());
            System.out.println("routingKey : " + envelope.getRoutingKey());
            String msg = new String(body, StandardCharsets.UTF_8);
            System.out.println("訊息內容 : " + msg);

            // 訊息確認
            channel.basicAck(envelope.getDeliveryTag(), false);
            System.out.println("訊息已確認");
        }
    };

我們將程式碼改成如此之後,可以再run一次消費者,可以看看效果:

訊息確認

再來看看RabbitMQ中的佇列情況:

訊息佇列狀態

從圖中我們可以看出訊息消費後已經成功被刪除了,其實大膽猜一猜,自動刪除應該是在我們的程式碼還沒執行之前就幫我們返回了確認,所以這就導致了訊息丟失的可能性。

我們採用手動確認的方式之後,可以先將邏輯處理完畢之後(可能出現異常的地方可以try-catch起來),把手動確認的程式碼放到最後一行,這樣如果出現異常情況導致這條訊息沒有被確認,那麼這條訊息會在之後被重新消費一遍。

後記

今天的內容就到這裡,下一篇將會我們將會撇棄傳統的手動建立連線的方式進行發訊息收訊息,而轉用Spring幫我們定義好的註解和Spring提供的RabbitTemplate,更方便的收發訊息。

訊息佇列呢,其實用法都是一樣的,只是各個開源訊息佇列的側重點稍有不同,我們應該根據我們自己的專案需求來決定我們應該選取什麼樣的訊息佇列來為我們的專案服務,這個專案選型的工作一般都是開發組長幫你們做了,一般是輪不到我們來做的,但是面試的時候可能會考察相關知識,所以這幾種訊息佇列我們都應該有所涉獵。

好了,以上就是本期的全部內容,感謝你能看到這裡,歡迎對本文點贊收藏與評論,?你們的每個點贊都是我創作的最大動力。

我是耳朵,一個一直想做知識輸出的偽文藝程式設計師,我們下期見。

本文程式碼:碼雲地址GitHub地址

相關文章