Spring Boot:使用Rabbit MQ訊息佇列

朝雨憶輕塵發表於2019-07-02

綜合概述

訊息佇列

訊息佇列就是一個訊息的連結串列,可以把訊息看作一個記錄,具有特定的格式以及特定的優先順序。對訊息佇列有寫許可權的程式可以向訊息佇列中按照一定的規則新增新訊息,對訊息佇列有讀許可權的程式則可以從訊息佇列中讀走訊息,而訊息佇列就是在訊息的傳輸過程中儲存訊息的容器,你可以簡單的把訊息佇列理解為類似快遞櫃,快遞員(訊息釋出者)往快遞櫃(訊息佇列)投遞物件(訊息),接受者(訊息訂閱者)從快遞櫃(訊息佇列)接收物件(訊息),當然訊息佇列往往還包含一些特定的訊息傳遞和接收機制。

訊息佇列作為分散式系統中重要的元件,可以有效解決應用耦合,非同步訊息,流量削鋒等系列問題,有利於實現高效能,高可用,可伸縮和最終一致性架構。目前使用較多的訊息佇列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ等,各種訊息佇列也都各有特點,比如Kafka提供高效能、高吞吐量,但可靠性有所欠缺,所以比較適合像日誌處理這類對效能要求高但對可靠性要求沒那麼嚴格的業務,再比如RabbitMQ支援了各種協議,實現較為臃腫,效能和吞吐量都一般,但卻提供了很好的可靠性,比較適合像銀行金融一類對可靠性要求較高的業務。

應用場景

以下簡單介紹幾個訊息佇列在實際應用中的使用場景(以下場景資料引用自網路)。

1 非同步處理

場景說明:使用者註冊後,需要發註冊郵件和註冊簡訊。傳統的做法有兩種 1.序列的方式;2.並行方式

(1)序列方式:將註冊資訊寫入資料庫成功後,傳送註冊郵件,再傳送註冊簡訊。以上三個任務全部完成後,返回給客戶端

 

(2)並行方式:將註冊資訊寫入資料庫成功後,傳送註冊郵件的同時,傳送註冊簡訊。以上三個任務完成後,返回給客戶端。與序列的差別是,並行的方式可以提高處理的時間

 

假設三個業務節點每個使用50毫秒鐘,不考慮網路等其他開銷,則序列方式的時間是150毫秒,並行的時間可能是100毫秒。

因為CPU在單位時間內處理的請求數是一定的,假設CPU1秒內吞吐量是100次。則序列方式1秒內CPU可處理的請求量是7次(1000/150)。並行方式處理的請求量是10次(1000/100)

小結:如以上案例描述,傳統的方式系統的效能(併發量,吞吐量,響應時間)會有瓶頸。如何解決這個問題呢?

引入訊息佇列,將不是必須的業務邏輯,非同步處理。改造後的架構如下:

 

按照以上約定,使用者的響應時間相當於是註冊資訊寫入資料庫的時間,也就是50毫秒。註冊郵件,傳送簡訊寫入訊息佇列後,直接返回,因此寫入訊息佇列的速度很快,基本可以忽略,因此使用者的響應時間可能是50毫秒。因此架構改變後,系統的吞吐量提高到每秒20 QPS。比序列提高了3倍,比並行提高了兩倍

2 應用解耦

場景說明:使用者下單後,訂單系統需要通知庫存系統。傳統的做法是,訂單系統呼叫庫存系統的介面。如下圖

 

傳統模式的缺點:

  • 假如庫存系統無法訪問,則訂單減庫存將失敗,從而導致訂單失敗

  • 訂單系統與庫存系統耦合

如何解決以上問題呢?引入應用訊息佇列後的方案,如下圖:

 

  • 訂單系統:使用者下單後,訂單系統完成持久化處理,將訊息寫入訊息佇列,返回使用者訂單下單成功

  • 庫存系統:訂閱下單的訊息,採用拉/推的方式,獲取下單資訊,庫存系統根據下單資訊,進行庫存操作

  • 假如:在下單時庫存系統不能正常使用。也不影響正常下單,因為下單後,訂單系統寫入訊息佇列就不再關心其他的後續操作了。實現訂單系統與庫存系統的應用解耦

3 流量削鋒

流量削鋒也是訊息佇列中的常用場景,一般在秒殺或團搶活動中使用廣泛

應用場景:秒殺活動,一般會因為流量過大,導致流量暴增,應用掛掉。為解決這個問題,一般需要在應用前端加入訊息佇列。

  • 可以控制活動的人數

  • 可以緩解短時間內高流量壓垮應用

 

  • 使用者的請求,伺服器接收後,首先寫入訊息佇列。假如訊息佇列長度超過最大數量,則直接拋棄使用者請求或跳轉到錯誤頁面

  • 秒殺業務根據訊息佇列中的請求資訊,再做後續處理

4 日誌處理

日誌處理是指將訊息佇列用在日誌處理中,比如Kafka的應用,解決大量日誌傳輸的問題。架構簡化如下

 

  • 日誌採集客戶端,負責日誌資料採集,定時寫受寫入Kafka佇列

  • Kafka訊息佇列,負責日誌資料的接收,儲存和轉發

  • 日誌處理應用:訂閱並消費kafka佇列中的日誌資料

以下是新浪kafka日誌處理應用案例:

 

(1)Kafka:接收使用者日誌的訊息佇列

(2)Logstash:做日誌解析,統一成JSON輸出給Elasticsearch

(3)Elasticsearch:實時日誌分析服務的核心技術,一個schemaless,實時的資料儲存服務,通過index組織資料,兼具強大的搜尋和統計功能

(4)Kibana:基於Elasticsearch的資料視覺化元件,超強的資料視覺化能力是眾多公司選擇ELK stack的重要原因

5 訊息通訊

訊息通訊是指,訊息佇列一般都內建了高效的通訊機制,因此也可以用在純的訊息通訊。比如實現點對點訊息佇列,或者聊天室等

點對點通訊:

 

客戶端A和客戶端B使用同一佇列,進行訊息通訊。

聊天室通訊:

 

客戶端A,客戶端B,客戶端N訂閱同一主題,進行訊息釋出和接收。實現類似聊天室效果。

以上實際是訊息佇列的兩種訊息模式,點對點或釋出訂閱模式。模型為示意圖,供參考。

Rabbit MQ

AMQP,即 Advanced Message Queuing Protocol,高階訊息佇列協議,是應用層協議的一個開放標準,為面向訊息的中介軟體設計。訊息中介軟體主要用於元件之間的解耦和通訊。AMQP的主要特徵是面向訊息、佇列、路由(包括點對點和釋出/訂閱)、可靠性和安全。
RabbitMQ是一個開源的AMQP實現,伺服器端用 Erlang 語言編寫,支援多種客戶端,如:Java、Python、Ruby、.NET、JMS、C、PHP、ActionScript、XMPP、STOMP等,支援AJAX。用於在分散式系統中儲存轉發訊息,具有很高的易用性和可用性。
 
接下來,我們先來了解幾個相關概念(以下相關介紹資料引用自網路)。

ConnectionFactory、Connection、Channel

ConnectionFactory、Connection、Channel都是RabbitMQ對外提供的API中最基本的物件。Connection是RabbitMQ的socket連結,它封裝了socket協議相關部分邏輯。ConnectionFactory為Connection的製造工廠。 Channel是我們與RabbitMQ打交道的最重要的一個介面,我們大部分的業務操作是在Channel這個介面中完成的,包括定義Queue、定義Exchange、繫結Queue與Exchange、釋出訊息等。

Queue

Queue(佇列)是RabbitMQ的內部物件,用於儲存訊息。

RabbitMQ中的訊息都只能儲存在Queue中,生產者(下圖中的P)生產訊息並最終投遞到Queue中,消費者(下圖中的C)可以從Queue中獲取訊息並消費。

生產者Send Message “A”被傳送到Queue中,消費者發現訊息佇列Queue中有訂閱的訊息,就會將這條訊息A讀取出來進行一些列的業務操作。這裡只是一個消費正對應一個佇列Queue,也可以多個消費者訂閱同一個佇列Queue,當然這裡就會將Queue裡面的訊息平分給其他的消費者,但是會存在一個一個問題就是如果每個訊息的處理時間不同,就會導致某些消費者一直在忙碌中,而有的消費者處理完了訊息後一直處於空閒狀態,因為前面已經提及到了Queue會平分這些訊息給相應的消費者。這裡我們就可以使用prefetchCount來限制每次傳送給消費者訊息的個數。詳情見下圖所示:

這裡的prefetchCount=1是指每次從Queue中傳送一條訊息來。等消費者處理完這條訊息後Queue會再傳送一條訊息給消費者。

Message acknowledgment

在實際應用中,可能會發生消費者收到Queue中的訊息,但沒有處理完成就當機(或出現其他意外)的情況,這種情況下就可能會導致訊息丟失。為了避免這種情況發生,我們可以要求消費者在消費完訊息後傳送一個回執給RabbitMQ,RabbitMQ收到訊息回執(Message acknowledgment)後才將該訊息從Queue中移除;如果RabbitMQ沒有收到回執並檢測到消費者的RabbitMQ連線斷開,則RabbitMQ會將該訊息傳送給其他消費者(如果存在多個消費者)進行處理。這裡不存在timeout概念,一個消費者處理訊息時間再長也不會導致該訊息被髮送給其他消費者,除非它的RabbitMQ連線斷開。 這裡會產生另外一個問題,如果我們的開發人員在處理完業務邏輯後,忘記傳送回執給RabbitMQ,這將會導致嚴重的bug——Queue中堆積的訊息會越來越多;消費者重啟後會重複消費這些訊息並重復執行業務邏輯…

另外pub message是沒有ack的。

Message durability

如果我們希望即使在RabbitMQ服務重啟的情況下,也不會丟失訊息,我們可以將Queue與Message都設定為可持久化的(durable),這樣可以保證絕大部分情況下我們的RabbitMQ訊息不會丟失。但依然解決不了小概率丟失事件的發生(比如RabbitMQ伺服器已經接收到生產者的訊息,但還沒來得及持久化該訊息時RabbitMQ伺服器就斷電了),如果我們需要對這種小概率事件也要管理起來,那麼我們要用到事務。由於這裡僅為RabbitMQ的簡單介紹,所以這裡將不講解RabbitMQ相關的事務。

Exchange

首先明確一點就是生產者產生的訊息並不是直接傳送給訊息佇列Queue的,而是要經過Exchange(交換器),由Exchange再將訊息路由到一個或多個Queue,當然這裡還會對不符合路由規則的訊息進行丟棄掉,這裡指的是後續要談到的Exchange Type。那麼Exchange是怎樣將訊息準確的推送到對應的Queue的呢?那麼這裡的功勞最大的當屬Binding,RabbitMQ是通過Binding將Exchange和Queue連結在一起,這樣Exchange就知道如何將訊息準確的推送到Queue中去。簡單示意圖如下所示:

        

在繫結(Binding)Exchange和Queue的同時,一般會指定一個Binding Key,生產者將訊息傳送給Exchange的時候,一般會產生一個Routing Key,當Routing Key和Binding Key對應上的時候,訊息就會傳送到對應的Queue中去。那麼Exchange有四種型別,不同的型別有著不同的策略。也就是表明不同的型別將決定繫結的Queue不同,換言之就是說生產者傳送了一個訊息,Routing Key的規則是A,那麼生產者會將Routing Key=A的訊息推送到Exchange中,這時候Exchange中會有自己的規則,對應的規則去篩選生產者發來的訊息,如果能夠對應上Exchange的內部規則就將訊息推送到對應的Queue中去。那麼接下來就來詳細講解下Exchange裡面型別。

Exchange Types

  • fanout

        fanout型別的Exchange路由規則非常簡單,它會把所有傳送到該Exchange的訊息路由到所有與它繫結的Queue中。

       

    上圖所示,生產者(P)生產訊息1將訊息1推送到Exchange,由於Exchange Type=fanout這時候會遵循fanout的規則將訊息推送到所有與它繫結Queue,也就是圖上的兩個Queue最後兩個消費者消費。

  • direct

        direct型別的Exchange路由規則也很簡單,它會把訊息路由到那些binding key與routing key完全匹配的Queue中

         

     當生產者(P)傳送訊息時Rotuing key=booking時,這時候將訊息傳送給Exchange,Exchange獲取到生產者傳送過來訊息後,會根據自身的規則進行與匹配相應的Queue,這時發現Queue1和Queue2都符合,就會將訊息傳送給這兩個佇列,如果我們以Rotuing key=create和Rotuing key=confirm傳送訊息時,這時訊息只會被推送到Queue2佇列中,其他Routing Key的訊息將會被丟棄。

  • topic

      前面提到的direct規則是嚴格意義上的匹配,換言之Routing Key必須與Binding Key相匹配的時候才將訊息傳送給Queue,那麼topic這個規則就是模糊匹配,可以通過萬用字元滿足一部分規則就可以傳送。它的約定是:

  1. routing key為一個句點號“. ”分隔的字串(我們將被句點號“. ”分隔開的每一段獨立的字串稱為一個單詞),如“stock.usd.nyse”、“nyse.vmw”、“quick.orange.rabbit”
  2. binding key與routing key一樣也是句點號“. ”分隔的字串
  3. binding key中可以存在兩種特殊字元“*”與“#”,用於做模糊匹配,其中“*”用於匹配一個單詞,“#”用於匹配多個單詞(可以是零個)

      

  當生產者傳送訊息Routing Key=F.C.E的時候,這時候只滿足Queue1,所以會被路由到Queue中,如果Routing Key=A.C.E這時候會被同是路由到Queue1和Queue2中,如果Routing Key=A.F.B時,這裡只會傳送一條訊息到Queue2中。

  • headers

        headers型別的Exchange不依賴於routing key與binding key的匹配規則來路由訊息,而是根據傳送的訊息內容中的headers屬性進行匹配。
在繫結Queue與Exchange時指定一組鍵值對;當訊息傳送到Exchange時,RabbitMQ會取到該訊息的headers(也是一個鍵值對的形式),對比其中的鍵值對是否完全匹配Queue與Exchange繫結時指定的鍵值對;如果完全匹配則訊息會路由到該Queue,否則不會路由到該Queue。

實現案例

首先,需要安裝Rabbit MQ,可以直接安裝,也可以用Docker安裝,這個網上教程很多,這裡就不再贅述了。

生成專案模板

為方便我們初始化專案,Spring Boot給我們提供一個專案模板生成網站。

1.  開啟瀏覽器,訪問:https://start.spring.io/

2.  根據頁面提示,選擇構建工具,開發語言,專案資訊等。

3.  點選 Generate the project,生成專案模板,生成之後會將壓縮包下載到本地。

4.  使用IDE匯入專案,我這裡使用Eclipse,通過匯入Maven專案的方式匯入。

新增相關依賴

清理掉不需要的測試類及測試依賴,新增 rabbitmq相關依賴。

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

下面給出完整的POM檔案。

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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.louis.springboot</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- web -->
        <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
        <!-- swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!-- rabbitmq -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

新增相關配置

新增一個swagger 配置類,在工程下新建 config 包並新增一個 SwaggerConfig 配置類。

SwaggerConfig.java

package com.louis.springboot.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi(){
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any()).build();
    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("Swagger API Doc")
                .description("This is a restful api document of Swagger.")
                .version("1.0")
                .build();
    }

}

修改application.properties檔名為application.yml,在其中新增RabbitMQ配置資訊,根據自己安裝的RabbitMQ配置。

application.yml

# rabbitmq配置
spring:
    rabbitmq:
      host: 127.0.0.1
      port: 5672
      username: guest
      password: guest

普通佇列模式

新建一個RabbitMQ配置類,並新增一個demoQueue佇列。

RabbitConfig.java

package com.louis.springboot.demo.config;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {

    /**
     * 定義demoQueue佇列
     * @return
     */
    @Bean
    public Queue demoString() {
        return new Queue("demoQueue");
    }
    
}

編寫一個訊息釋出者,並編寫一個傳送方法,通過AmqpTemplate往"demoQueue"傳送訊息。

RabbitProducer.java

package com.louis.springboot.demo.mq;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class RabbitProducer {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public void sendDemoQueue() {
        Date date = new Date();
        String dateString = new SimpleDateFormat("YYYY-mm-DD hh:MM:ss").format(date);
        System.out.println("[demoQueue] send msg: " + dateString);  
        // 第一個引數為剛剛定義的佇列名稱
        this.rabbitTemplate.convertAndSend("demoQueue", dateString);
    }
}

編寫一個訊息消費者,通過@RabbitListener(queues = "demoQueue")註解監聽"demoQueue"佇列,並用@RabbitHandler註解相關方法,這樣在在佇列收到訊息之後,交友@RabbitHandler註解的方法進行處理。

DemoQueueConsumer.java

package com.louis.springboot.demo.mq;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;


@Component
@RabbitListener(queues = "demoQueue")
public class DemoQueueConsumer {

    /**
     * 訊息消費
     * @RabbitHandler 代表此方法為接受到訊息後的處理方法
     */
    @RabbitHandler
    public void recieved(String msg) {
        System.out.println("[demoQueue] recieved message: " + msg);
    }

}

編寫一個控制器,注入RabbitProducer呼叫相關訊息傳送方法,方便通過介面觸發訊息傳送。

RabbitMqController.java

package com.louis.springboot.demo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.louis.springboot.demo.mq.RabbitProducer;

@RestController
public class RabbitMqController {

    @Autowired
    private RabbitProducer rabbitProducer;

    @GetMapping("/sendDemoQueue")
    public Object sendDemoQueue() {
        rabbitProducer.sendDemoQueue();
        return "success";
    }
}

編譯並啟動應用,開啟瀏覽器,訪問:http://localhost:8080/swagger-ui.html,進入swagger介面文件介面。

呼叫兩次sendDemoQueue介面,在控制檯可以看到我們輸出的資訊,說明訊息已經成功傳送並被消費。

[demoQueue] send msg: 2019-58-183 04:07:38
[demoQueue] recieved message: 2019-58-183 04:07:38
[demoQueue] send msg: 2019-01-183 05:07:05
[demoQueue] recieved message: 2019-01-183 05:07:05

Fanout廣播模式

Fanout其實就是廣播模式,只要跟它繫結的佇列都會通知並且接受到訊息。修改配置類,在RabbitConfig中新增如下fanout模式的佇列跟交換機資訊。在程式碼中我們配置了三個佇列名、一個fanout交換機,並且將這三個佇列繫結到了fanout交換器上。只要我們往這個交換機生產新的訊息,那麼這三個佇列都會收到。

RabbitConfig.java

    //=================== fanout廣播模式  ====================

    @Bean
    public Queue fanoutA() {
        return new Queue("fanout.a");
    }

    @Bean
    public Queue fanoutB() {
        return new Queue("fanout.b");
    }

    @Bean
    public Queue fanoutC() {
        return new Queue("fanout.c");
    }

    /**
     * 定義個fanout交換器
     * @return
     */
    @Bean
    FanoutExchange fanoutExchange() {
        // 定義一個名為fanoutExchange的fanout交換器
        return new FanoutExchange("fanoutExchange");
    }

    /**
     * 將定義的fanoutA佇列與fanoutExchange交換機繫結
     * @return
     */
    @Bean
    public Binding bindingExchangeWithA() {
        return BindingBuilder.bind(fanoutA()).to(fanoutExchange());
    }

    /**
     * 將定義的fanoutB佇列與fanoutExchange交換機繫結
     * @return
     */
    @Bean
    public Binding bindingExchangeWithB() {
        return BindingBuilder.bind(fanoutB()).to(fanoutExchange());
    }

    /**
     * 將定義的fanoutC佇列與fanoutExchange交換機繫結
     * @return
     */
    @Bean
    public Binding bindingExchangeWithC() {
        return BindingBuilder.bind(fanoutC()).to(fanoutExchange());
    }

然後我們在RabbitProducer中新增一個sendFanout方法,用來向fanout佇列傳送訊息。

RabbitProducer.java

public void sendFanout() {
    Date date = new Date();
    String dateString = new SimpleDateFormat("YYYY-mm-DD hh:MM:ss").format(date);
    System.out.println("[fanout] send msg:" + dateString);
    // 注意 第一個引數是我們交換機的名稱 ,第二個引數是routerKey 我們不用管空著就可以,第三個是你要傳送的訊息
    this.rabbitTemplate.convertAndSend("fanoutExchange", "", dateString);
}

同樣的,在控制器裡新增一個訪問介面。

RabbitMqController.java

@GetMapping("/sendFanout")
public Object sendFanout() {
    rabbitProducer.sendFanout();
    return "success";
}

接著針對三個廣播佇列分別編寫一個訊息消費者,指定佇列和處理函式。

FanoutAConsumer.java

package com.louis.springboot.demo.mq;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@RabbitListener(queues = "fanout.a")
public class FanoutAConsumer {

    /**
     * 訊息消費
     * @RabbitHandler 代表此方法為接受到訊息後的處理方法
     */
    @RabbitHandler
    public void recieved(String msg) {
        System.out.println("[fanout.a] recieved message: " + msg);
    }
}

FanoutBConsumer.java

package com.louis.springboot.demo.mq;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@RabbitListener(queues = "fanout.b")
public class FanoutBConsumer {

    /**
     * 訊息消費
     * @RabbitHandler 代表此方法為接受到訊息後的處理方法
     */
    @RabbitHandler
    public void recieved(String msg) {
        System.out.println("[fanout.b] recieved message: " + msg);
    }
}

FanoutCConsumer.java

package com.louis.springboot.demo.mq;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@RabbitListener(queues = "fanout.c")
public class FanoutCConsumer {

    /**
     * 訊息消費
     * @RabbitHandler 代表此方法為接受到訊息後的處理方法
     */
    @RabbitHandler
    public void recieved(String msg) {
        System.out.println("[fanout.c] recieved message: " + msg);
    }
}

重新啟動應用,呼叫sendFanout介面,通過控制檯可以看到訊息傳送之後,a, b, c三個佇列都收到了訊息。

[fanout] send msg:2019-47-183 05:07:12
[fanout.c] recieved message: 2019-47-183 05:07:12
[fanout.b] recieved message: 2019-47-183 05:07:12
[fanout.a] recieved message: 2019-47-183 05:07:12

Topic主題模式

利用topic模式可以實現模糊匹配,同樣的,在RabbitConfig中配置topic佇列跟交換器,注意的是這裡需要多配置一個bindingKey。

RabbitConfig.java

    //=================== topic主題模式  ====================

    @Bean
    public Queue topiocA() {
        return new Queue("topic.a");
    }

    @Bean
    public Queue topicB() {
        return new Queue("topic.b");
    }

    @Bean
    public Queue topicC() {
        return new Queue("topic.c");
    }

    /**
     * 定義個topic交換器
     * @return
     */
    @Bean
    TopicExchange topicExchange() {
        // 定義一個名為fanoutExchange的fanout交換器
        return new TopicExchange("topicExchange");
    }

    /**
     * 將定義的topicA佇列與topicExchange交換機繫結
     * @return
     */
    @Bean
    public Binding bindingTopicExchangeWithA() {
        return BindingBuilder.bind(topiocA()).to(topicExchange()).with("topic.msg");
    }

    /**
     * 將定義的topicB佇列與topicExchange交換機繫結
     * @return
     */
    @Bean
    public Binding bindingTopicExchangeWithB() {
        return BindingBuilder.bind(topicB()).to(topicExchange()).with("topic.#");
    }

    /**
     * 將定義的topicC佇列與topicExchange交換機繫結
     * @return
     */
    @Bean
    public Binding bindingTopicExchangeWithC() {
        return BindingBuilder.bind(topicC()).to(topicExchange()).with("topic.*.z");
    }

上述配置中:

topicA的key為topic.msg 那麼他只會接收包含topic.msg的訊息

topicB的key為topic.#那麼他只會接收topic開頭的訊息

topicC的key為topic.*.z那麼他只會接收topic.x.z這樣格式的訊息

然後修改RabbitProducer,在其中新增如下三個方法,如方法名所示,分別根據匹配規則傳送到A\B,B,B\C佇列。

RabbitProducer.java

public void sendTopicTopicAB() {
    Date date = new Date();
    String dateString = new SimpleDateFormat("YYYY-mm-DD hh:MM:ss").format(date);
    dateString = "[topic.msg] send msg:" + dateString;
    System.out.println(dateString);
    // 注意 第一個引數是我們交換機的名稱 ,第二個引數是routerKey topic.msg,第三個是你要傳送的訊息
    // 這條資訊將會被 topic.a  topic.b接收
    this.rabbitTemplate.convertAndSend("topicExchange", "topic.msg", dateString);
}

public void sendTopicTopicB() {
    Date date = new Date();
    String dateString = new SimpleDateFormat("YYYY-mm-DD hh:MM:ss").format(date);
    dateString = "[topic.good.msg] send msg:" + dateString;
    System.out.println(dateString);
    // 注意 第一個引數是我們交換機的名稱 ,第二個引數是routerKey ,第三個是你要傳送的訊息
    // 這條資訊將會被topic.b接收
    this.rabbitTemplate.convertAndSend("topicExchange", "topic.good.msg", dateString);
}

public void sendTopicTopicBC() {
    Date date = new Date();
    String dateString = new SimpleDateFormat("YYYY-mm-DD hh:MM:ss").format(date);
    dateString = "[topic.m.z] send msg:" + dateString;
    System.out.println(dateString);
    // 注意 第一個引數是我們交換機的名稱 ,第二個引數是routerKey ,第三個是你要傳送的訊息
    // 這條資訊將會被topic.b、topic.c接收
    this.rabbitTemplate.convertAndSend("topicExchange", "topic.m.z", dateString);
}

同樣的,在控制器裡面新增傳送服務對應的介面。

RabbitMqController.java

@GetMapping("/sendTopicTopicAB")
public Object sendTopicTopicAB() {
    rabbitProducer.sendTopicTopicAB();
    return "success";
}

@GetMapping("/sendTopicTopicB")
public Object sendTopicTopicB() {
    rabbitProducer.sendTopicTopicB();
    return "success";
}

@GetMapping("/sendTopicTopicBC")
public Object sendTopicTopicBC() {
    rabbitProducer.sendTopicTopicBC();
    return "success";
}

接著針對三個主題佇列編寫對應的訊息消費者。

TopicAConsumer.java

package com.louis.springboot.demo.mq;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@RabbitListener(queues = "topic.a")
public class TopicAConsumer {

    /**
     * 訊息消費
     * @RabbitHandler 代表此方法為接受到訊息後的處理方法
     */
    @RabbitHandler
    public void recieved(String msg) {
        System.out.println("[topic.a] recieved message:" + msg);
    }
}

TopicBConsumer.java

package com.louis.springboot.demo.mq;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@RabbitListener(queues = "topic.b")
public class TopicBConsumer {

    /**
     * 訊息消費
     * @RabbitHandler 代表此方法為接受到訊息後的處理方法
     */
    @RabbitHandler
    public void recieved(String msg) {
        System.out.println("[topic.b] recieved message:" + msg);
    }
}

TopicCConsumer.java

package com.louis.springboot.demo.mq;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@RabbitListener(queues = "topic.c")
public class TopicCConsumer {

    /**
     * 訊息消費
     * @RabbitHandler 代表此方法為接受到訊息後的處理方法
     */
    @RabbitHandler
    public void recieved(String msg) {
        System.out.println("[topic.c] recieved message:" + msg);
    }
}

重啟應用,呼叫sendTopicTopicAB介面,經過匹配,route key為“topic.msg”的訊息被髮送到了topic.a和topic.b。

[topic.msg] send msg:2019-12-183 06:07:22
[topic.b] recieved message:[topic.msg] send msg:2019-12-183 06:07:22
[topic.a] recieved message:[topic.msg] send msg:2019-12-183 06:07:22

呼叫sendTopicTopicB介面,經過匹配,route key為“topic.good.msg”的訊息被髮送到了topic.b。

[topic.good.msg] send msg:2019-15-183 06:07:23
[topic.b] recieved message:[topic.good.msg] send msg:2019-15-183 06:07:23

呼叫sendTopicTopicBC介面,經過匹配,route key為“topic.m.z”的訊息被髮送到了topic.b和topic.c。

[topic.m.z] send msg:2019-16-183 06:07:09
[topic.b] recieved message:[topic.m.z] send msg:2019-16-183 06:07:09
[topic.c] recieved message:[topic.m.z] send msg:2019-16-183 06:07:09

 

 

參考資料

官方網站:https://www.rabbitmq.com/

百度百科:https://baike.baidu.com/item/rabbitmq/9372144?fr=aladdin

中文教程:http://rabbitmq.mr-ping.com/description.html

相關導航

Spring Boot 系列教程目錄導航

Spring Boot:快速入門教程

Spring Boot:整合Swagger文件

Spring Boot:整合MyBatis框架

Spring Boot:實現MyBatis分頁

原始碼下載

碼雲:https://gitee.com/liuge1988/spring-boot-demo.git


作者:朝雨憶輕塵
出處:https://www.cnblogs.com/xifengxiaoma/ 
版權所有,歡迎轉載,轉載請註明原文作者及出處。

相關文章