RabbitMq消息模型總結+實戰

相關文章

Linux中安裝RabbitMQ
Linux環境搭建Rabbitmq集羣
Rabbitmq常用命令
爲什麼要用消息隊列+各個消息隊列框架該如何選擇?

什麼是RabbitMq

所有 MQ 產品從模型抽象上來說都是一樣的過程:
消費者(consumer)訂閱某個隊列。生產者(pr‘’oducer)創建消息,然後發佈到隊列(queue)中,最後將消息發送到監聽的消費者。
不同的MQ產品有不同的機制,RabbitMQ實際基於AMQP協議的一個開源實現,因此RabbitMQ內部也是AMQP的基本概念。

RabbitMQ的內部機制如下:
在這裏插入圖片描述

1、結構說明:

1、Message
消息,消息是不具體的,它由消息頭和消息體組成。消息體是不透明的,而消息頭則由一系列的可選屬性組成,這些屬性包括routing-key(路由鍵)、priority(相對於其他消息的優先權)、delivery-mode(指出該消息可能需要持久性存儲)等。
2、Publisher
消息的生產者,也是一個向交換器發佈消息的客戶端應用程序。
3、Exchange
交換器,用來接收生產者發送的消息並將這些消息路由給服務器中的隊列。
4、Binding
綁定,用於消息隊列和交換器之間的關聯。一個綁定就是基於路由鍵將交換器和消息隊列連接起來的路由規則,所以可以將交換器理解成一個由綁定構成的路由表。
5、Queue
消息隊列,用來保存消息直到發送給消費者。它是消息的容器,也是消息的終點。一個消息可投入一個或多個隊列。消息一直在隊列裏面,等待消費者連接到這個隊列將其取走。
那麼誰應該負責創建這個queue呢?是Consumer,還是Producer?
如果queue不存在,當然Consumer不會得到任何的Message。但是如果queue不存在,那麼Producer Publish的Message會被丟棄。所以,還是爲了數據不丟失,Consumer和Producer都try to create the queue!反正不管怎麼樣,這個接口都不會出問題。
6、Connection
網絡連接,比如一個TCP連接。
7、Channel
信道,多路複用連接中的一條獨立的雙向數據流通道。信道是建立在真實的TCP連接內地虛擬連接,AMQP 命令都是通過信道發出去的,不管是發佈消息、訂閱隊列還是接收消息,這些動作都是通過信道完成。因爲對於操作系統來說建立和銷燬 TCP 都是非常昂貴的開銷,所以引入了信道的概念,以複用一條 TCP 連接。
8、Consumer
消息的消費者,表示一個從消息隊列中取得消息的客戶端應用程序。
9、Virtual Host
虛擬主機,表示一批交換器、消息隊列和相關對象。虛擬主機是共享相同的身份認證和加密環境的獨立服務器域。每個 vhost 本質上就是一個 mini 版的 RabbitMQ 服務器,擁有自己的隊列、交換器、綁定和權限機制。vhost 是 AMQP 概念的基礎,必須在連接時指定,RabbitMQ 默認的 vhost 是 / 。
10、Broker
表示消息隊列服務器實體。

一、基本消息模型

對於基本消息模型,我們可以只做一個入門概念的瞭解,它並沒有使用交換器和消息綁定機制,消費者生產消息放到隊列中,即可進行自己後續的操作,隊列中的消息會被消費者監聽到,消費者監聽到有消息存在後,變會獲取消息執行任務。

應用場景:發短信、發郵件。
在這裏插入圖片描述

1、角色說明

P(producer/ publisher):生產者,一個發送消息的用戶應用程序。

C(consumer):消費者,消費和接收有類似的意思,消費者是一個主要用來等待接收消息的用戶應用程序。

隊列(紅色區域):rabbitmq內部類似於郵箱的一個概念。雖然消息流經rabbitmq和你的應用程序,但是它們只能存儲在隊列中。隊列只受主機的內存和磁盤限制,實質上是一個大的消息緩衝區。許多生產者可以發送消息到一個隊列,許多消費者可以嘗試從一個隊列接收數據。

2、 實例

想要使用rabbitmq,必須先引入先關依賴,spring提供了spring-boot-starter-amqp依賴,只需要簡單的配置即可與spring無縫整合。
導入依賴

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

建立RabbitMQ的連接類

public class ConnectionUtil {
    /**
     * 建立與RabbitMQ的連接
     * @return
     * @throws Exception
     */
    public static Connection getConnection() throws Exception {
        //定義連接工廠
        ConnectionFactory factory = new ConnectionFactory();
        //設置服務地址
        factory.setHost("192.168.237.139");
        //端口
        factory.setPort(5672);
        //設置賬號信息,用戶名、密碼、vhost
        factory.setVirtualHost("/");
        factory.setUsername("zhy");
        factory.setPassword("zhy");
        // 通過工程獲取連接
        Connection connection = factory.newConnection();
        return connection;
    }

}

生產者:

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

消費者1:

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 即消息體
                //int a=1/0;
                String msg = new String(body);
                System.out.println(" [x] received : " + msg + "!");
            }
        };
        // 監聽隊列,第二個參數:是否自動進行消息確認,這裏選擇true,即只要拿到消息就立即確認,從消息隊列中移除。
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }

上面這種監聽隊列模式,存在一定的弊端,比如,消費者在獲得消息,進行任務處理的時候服務器異常,但是這時候消息隊列中的消息已經被確認消費,消息隊列中已經沒有了這條消息,這將導致消息丟失的現象。
比如我們在消費者代碼中寫入除零異常:int a=1/0;

消費之前隊列內容:
在這裏插入圖片描述
執行消費後,雖然代碼報異常,但是消息仍然被消費:
在這裏插入圖片描述
在這裏插入圖片描述
解決方式:
我們可以不進行自動確認,即自己進行手動ack確認,如消費者2:

手動ack確認

需要添加代碼:
channel.basicAck(envelope.getDeliveryTag(), false);

消費者2:

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 即消息體
                int i=1/0;
                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);
    }

我們執行消費者2代碼,消息沒有被消費時,消息隊列中有內容:
在這裏插入圖片描述
執行消費者代碼後,報異常,但是消息隊列中的消息並沒有被消費:
在這裏插入圖片描述
在這裏插入圖片描述

二、work消息模型(競爭消費者模式)

在這裏插入圖片描述

角色說明

P:生產者:任務的發佈者

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

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

工作隊列,又稱任務隊列。主要思想就是避免執行資源密集型任務時,必須等待它執行完成。
對於資源密集型任務,我們可以設置多個消費者來監聽同一個隊列。並且當多個消費者消費消息的速度不一致時,我們開可以開啓能者多勞模式,即消費速度快的多消費,消費速度慢的少消費。這樣來提高資源密集型任務的執行效率,避免消息堆積

應用場景:秒殺、搶紅包。

實例

在生產者中同時生產50個任務,兩個消費者進行消費,其中一個消費者做睡眠處理,模擬消費時出現卡頓的現象(保證消費者先啓動,然後再啓動生產者),運行結果會發現,兩個消費者是輪詢進行的,沒有被設置卡頓的消費者很快輪詢結束自己的任務,但是被設置睡眠的消費者,還在一點一點的執行着自己的任務。

生產者

// 生產者
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();
    }
}

消費者1

// 消費者1
public class Recv {
    private final static String QUEUE_NAME = "test_work_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(" [消費者1] received : " + msg + "!");
                try {
                    // 模擬完成任務的耗時:1000ms
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
                // 手動ACK
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 監聽隊列。
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

消費者2

//消費者2
public class Recv2 {
    private final static String QUEUE_NAME = "test_work_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(" [消費者2] received : " + msg + "!");
                // 手動ACK
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 監聽隊列。
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

運行結果:

在這裏插入圖片描述
在這裏插入圖片描述
結果中,兩個消費者各自消費了25個任務,實現了任務的分發,當消費者2已經消費結束後,消費者1由於設置了睡眠時間,導致消費時間很慢。

爲了解決這個問題,我們可以開啓能者多勞,使用basicQos方法和prefetchCount = 1設置。 這告訴RabbitMQ一次不要向工作人員發送多於一條消息。 或者換句話說,不要向工作人員發送新消息,直到它處理並確認了前一個消息。 相反,它會將其分派給不是仍然忙碌的下一個工作人員。

在兩個消費者中加上如下代碼:

// 設置每個消費者同時只能處理一條消息
        channel.basicQos(1);

在這裏插入圖片描述
重新運行消費者和生產者:
在這裏插入圖片描述
結果中,消費比較快的消費者多消費,體現能者多勞。

注意
1、Queue的消息只能被同一個消費者消費,如果沒有消費監聽隊列那麼消息會存放到隊列中持久化保存,直到有消費者來消費這個消息,如果以有消費者監聽隊列則立即消費發送到隊列中的消息.

2、Queue的消息可以保證每個消息都一定能被消費.

三、訂閱模型

AMQP消息路由

在說訂閱模型之前我們先來了解一下AMQP中的消息路由。

RabbitMQ是基於AMQP協議的一個開源實現,AMQP 中增加了 Exchange 和 Binding 的角色。生產者把消息發佈到 Exchange 上,消息最終到達隊列並被消費者接收,而 Binding 決定交換器的消息應該發送到那個隊列。
在這裏插入圖片描述
Spring最擅長的事情就是封裝,把他人的框架進行封裝和整合。

Spring爲AMQP提供了統一的消息處理模板:AmqpTemplate。

Exchange類型

Exchange分發消息時根據類型的不同分發策略有區別,目前共四種類型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由鍵,此外 headers 交換器和 direct 交換器完全一致,但性能差很多,目前幾乎用不到了,所以直接看另外三種類型。

1、direct

消息中的路由鍵(routing key)如果和 Binding 中的 binding key 一致, 交換器就將消息發到對應的隊列中。路由鍵與隊列名完全匹配,如一個隊列綁定到交換機要求路由鍵爲“dog”,則只轉發 routing key 標記爲“dog”的消息,不會轉發“dog.puppy”,也不會轉發“dog.guard”等等。它是完全匹配、單播的模式。消費者只需監聽某個隊列以後,就會獲取隊列中的消息。
在這裏插入圖片描述

實例

關於訂閱模型的實例我們通過構建兩個工程來實現,一個消費者工程,一個生產者工程。

生產者工程

導入依賴

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

編寫配置文件

#配置rabbitmq的相關鏈接信息(單機版)
spring.rabbitmq.host=192.168.237.135
spring.rabbitmq.port=5672
spring.rabbitmq.username=zhy
spring.rabbitmq.password=zhy

編寫一個 config文件類,用來向spring容器中注入交換機、隊列、和交換機與隊列的綁定

@Configuration
public class RabbitmqConfig {

    //配置一個Direct類型的交換機
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("bootDirectExchange");
    }

    //配置一個隊列
    @Bean
    public Queue directQueue()  {
        return new Queue("bootDirectQueue");
    }

    /**
     * 配置一個隊列和交換機綁定
     * @param directQueue 需要綁定的隊列對象,參數名必須要與某個@Bean的方法名完全相同保證自動注入
     * @param directExchange 需要綁定的交換機的對象,參數名必須要與某個@Bean的方法名完全相同保證自動注入
     * @return
     */
    @Bean
    public Binding directBinding(Queue directQueue,DirectExchange directExchange){
        // 完成綁定
        return BindingBuilder.bind(directQueue).to(directExchange).with("bootDirectRoutingKey");
    }
   }

編寫service接口

public interface SendService {
    void sendMessage(String message);
}

編寫service實現類

@Service("sendService")
public class SendServiceImpl implements SendService {
//    注入amqp模板類,利用這個對象來發送和接受消息
    @Resource
    private AmqpTemplate amqpTemplate;

    @Override
    public void sendMessage(String message){
        amqpTemplate.convertAndSend("bootDirectExchange","bootDirectRoutingKey",message);
    }
}

啓動類

@SpringBootApplication
public class RabbitmqSendBootApplication {

    public static void main(String[] args) {
        ApplicationContext ac= SpringApplication.run(RabbitmqSendBootApplication.class, args);
        SendService service= (SendService) ac.getBean("sendService");

        service.sendMessage("boot測試數據");
    }

}
消費者工程

同樣導入依賴、編寫配置文件、編寫一個 config文件類,代碼與生產者工程代碼相同,這裏不再重複。

編寫 接口

public interface ReceiveService {
   void receive();
}

接口實現類

@Service("receiveService")
public class ReceiveServiceImpl implements ReceiveService {
//    注入amqp模板類,利用這個對象來發送和接受消息
    @Resource
    private AmqpTemplate amqpTemplate;

    // 不是不間斷的接收消息,每執行一次這個方法只能接收一次消息,如果有新的消息進入則不會自動接收消息
    @Override
    public void receive() {
        String message= (String) amqpTemplate.receiveAndConvert("bootDirectQueue");
        System.out.println(message);
    }

啓動類

@SpringBootApplication
public class RabbitmqReceiveBootApplication {
    public static void main(String[] args) {
        ApplicationContext ac= SpringApplication.run(RabbitmqReceiveBootApplication.class, args);
        ReceiveService service= (ReceiveService) ac.getBean("receiveService");
        
        service.receive();
    }
}

分別啓動生產者和消費者,我們將看到消費者能夠拿到隊列中的消息:
在這裏插入圖片描述
但是這種寫法並不完善,因爲消費者此時並不能不間斷的接收消息,每次執行一次receive這個方法,只能接收一次消息,如果有新的消息進入則不會自動接收消息。
比如生產者我們進行多條消息發送:
在這裏插入圖片描述
重新啓動生產者和消費者,發現隊列中只被消費了一條信息:
在這裏插入圖片描述
消費者只拿到一條數據:
在這裏插入圖片描述
爲了解決這個問題,我們可以在方法上添加一個監聽的註解@RabbitListener,用來持續性的自動接收消息。

   // 不是不間斷的接收消息,沒執行一次這個方法只能接收一次消息,如果有新的消息進入則不會自動接收消息
//    @Override
//    public void receive() {
//        String message= (String) amqpTemplate.receiveAndConvert("bootDirectQueue");
//        System.out.println(message);
//    }
@RabbitListener(queues = {"bootDirectQueue"})
    /**
     * 該註解用於標記當前方法是一個rabbitmq的消息監聽方法,作用是持續性的自動接收消息,不需要手動調用spring會自動運行監聽
     * 參數queues用於指定一個 已經存在的隊列名,用於進行隊列的監聽
     * 注意:如果當前監聽方法正常結束spring就會自動確認消息,如果出現異常則不會確認消息
     *  因此在消息處理時我們需要做好消息的防止重複處理工作
      */
    public void directReceive(String message){
        System.out.println("監聽器接收的消息---"+message);
    }

在這裏插入圖片描述
重新運行程序,發現能夠持續 行消費了:
在這裏插入圖片描述

注意

注意:
1、使用direct消息模式時必須要指定RoutingKey(路由鍵),將指定的消息綁定到指定的路由鍵上
2、使用Exchange的direct模式時接收者的RoutingKey必須要與發送時的RoutingKey完全一致否則無法獲取消息
3、接收消息時隊列名也必須要和發送消息時的完全一致

2、fanout

每個發到 fanout 類型交換器的消息都會分到所有綁定的隊列上去。fanout 交換器不處理路由鍵,沒有Routingkey以及Bindingkey的概念,只是簡單的將隊列綁定到交換器上,每個發送到交換器的消息都會被轉發到與該交換器綁定的所有隊列上。很像子網廣播,每臺子網內的主機都獲得了一份複製的消息。fanout 類型轉發消息是最快的。

缺點
必須現在消費中監聽隊列,否則如果消息先發送了,那麼消費者永遠錯過消息。
優點
速度最快。

適合於在同一個功能不同進程來獲取數據,如app消息推送。
在這裏插入圖片描述

實例

我們改造一下上面的工程

生產者工程:

在confing文件類中添加配置一個fanout類型的交換機

    /**
     * 配置一個fanout類型的交換機
     * @return
     */
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("fanoutExchange");
    }

service接口和實現類中添加發送fanoutmessage的方法 :

    public void sendFanoutmessage(String message){
        amqpTemplate.convertAndSend("fanoutExchange","",message);
    }

啓動類中調用該方法,註解掉原來的方法調用避免出現混淆:

//        for (int i = 0; i < 10; i++) {
//            service.sendMessage("boot測試數據 "+i);
//        }
service.sendFanoutmessage("boot的fandout測試數據");
消費者工程:

在service類中添加兩個fanout的消費者方法(爲了避免混淆可以將 原來的 方法註釋掉):

@RabbitListener(bindings = {
            // @QueueBinding註解要完成隊列和交換機
            @QueueBinding(value = @Queue(),//@Queue創建一個隊列(沒有指定參數則表示創建一個隨機隊列)
                    exchange = @Exchange(name = "fanoutExchange",type = "fanout") //創建一個交換機
            )})
    public void fanoutReceive01(String message){
        System.out.println("fanoutReceive01監聽器接收的消息---"+message);
    }

    @RabbitListener(bindings = {
            // @QueueBinding註解要完成隊列和交換機
            @QueueBinding(value = @Queue(),//@Queue創建一個隊列(沒有指定參數則表示創建一個隨機隊列)
                    exchange = @Exchange(name = "fanoutExchange",type = "fanout") //創建一個交換機
            )})
    public void fanoutReceive02(String message){
        System.out.println("fanoutReceive02監聽器接收的消息---"+message);
    }

先啓動消費者進行監聽,後啓動生產者工程:
查看rabbitmq中有fanout交換機的存在
在這裏插入圖片描述
消費者進程中,兩個消費方法都拿到了消息:
在這裏插入圖片描述

注意

1、fanout模式的消息需要將一個消息同時綁定到多個隊列中因此這裏不能創建並指定某個隊列
2、使用fanout模式獲取消息時不需要綁定特定的隊列名稱,獲取一個隨機的隊列名稱,然後綁定到指定的Exchange即可獲取消息。
3、這種模式中可以同時啓動多個接收者只要都綁定到同一個Exchang即可讓所有接收者同時接收同一個消息是一種廣播的消息機制。
4、必須現在消費中監聽隊列,即消費者先啓動,否則如果消息先發送了,那麼消費者永遠錯過消息。

3、topic

topic 交換器通過模式匹配分配消息的路由鍵屬性,將路由鍵和某個模式進行匹配,此時隊列需要綁定到一個模式上。它將路由鍵和綁定鍵的字符串切分成單詞,這些單詞之間用點隔開。它同樣也會識別兩個通配符:符號“#”和符號“*”。#匹配0個或多個單詞,“*”匹配不多不少一個單詞。
topic交換機基本概念和使用與fanout相同,但是需要使用bindingkey。
topic也會丟失消息,所以需要先啓動消費者監聽。
topic更適合不同的功能模塊來接收同一個消息,如訂單成功。
在這裏插入圖片描述
AMQP 協議中的核心思想就是生產者和消費者的解耦,生產者從不直接將消息發送給隊列。生產者通常不知道是否一個消息會被髮送到隊列中,只是將消息發送到一個交換機。先由 Exchange 來接收,然後 Exchange 按照特定的策略轉發到 Queue 進行存儲。Exchange 就類似於一個交換機,將各個消息分發到相應的隊列中。

在實際應用中我們只需要定義好 Exchange 的路由策略,而生產者則不需要關心消息會發送到哪個 Queue 或被哪些 Consumer 消費。在這種模式下生產者只面向 Exchange 發佈消息,消費者只面向 Queue 消費消息,Exchange 定義了消息路由到 Queue 的規則,將各個層面的消息傳遞隔離開,使每一層只需要關心自己面向的下一層,降低了整體的耦合度。

實例

改動上面的工程

生產者工程

在config文件類中添加topic類型的 交換機(爲了避免混淆,可以註釋掉原來的交換機)

    /**
     * 配置一個Topic類型的交換機
     * @return
     */
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange("topicExchange");
    }

在service接口和實現類中,添加 發送topicmessage消息的方法:

   public void sendTopicmessage(String message){
        amqpTemplate.convertAndSend("topicExchange","aa.bb",message);
    }

啓動類中調用發送消息的方法

//        for (int i = 0; i < 10; i++) {
//            service.sendMessage("boot測試數據 "+i);
//        }

//        service.sendFanoutmessage("boot的fandout測試數據");
service.sendTopicmessage("boot的topic測試數據key 爲 aa.bb");
消費者

在service類中,添加三個監聽topic類型交換機的方法,並分別爲其指定路由鍵的匹配規則

@RabbitListener(bindings = {
            // @QueueBinding註解要完成隊列和交換機
            @QueueBinding(value = @Queue("topic01"),//@Queue創建一個隊列(沒有指定參數則表示創建一個隨機隊列)
                    key = {"aa"},
                    exchange = @Exchange(name = "topicExchange",type = "topic") //創建一個交換機
            )})
    public void topicReceive01(String message){
        System.out.println("topic01消費者 ---aa---"+message);
    }

    @RabbitListener(bindings = {
            // @QueueBinding註解要完成隊列和交換機
            @QueueBinding(value = @Queue("topic02"),//@Queue創建一個隊列(沒有指定參數則表示創建一個隨機隊列)
                    key = {"aa.*"},
                    exchange = @Exchange(name = "topicExchange",type = "topic") //創建一個交換機
            )})
    public void topicReceive02(String message){
        System.out.println("topic02消費者 ---aa.*---"+message);
    }
    @RabbitListener(bindings = {
            // @QueueBinding註解要完成隊列和交換機
            @QueueBinding(value = @Queue("topic03"),//@Queue創建一個隊列(沒有指定參數則表示創建一個隨機隊列)
                    key = {"aa.#"},
                    exchange = @Exchange(name = "topicExchange",type = "topic") //創建一個交換機
            )})
    public void topicReceive03(String message){
        System.out.println("topic03消費者 ---aa.#---"+message);
    }

先啓動消費者進程,進行監聽,在啓動生產者進程:

在這裏插入圖片描述
消費者消費情況:
在這裏插入圖片描述

注意

1、在topic模式中必須要指定Routingkey,並且可以同時指定多層的RoutingKey,每個層次之間使用 點分隔即可 例如 test.myRoutingKey
2、Topic模式的消息接收時必須要指定RoutingKey並且可以使用# 和 *來做統配符號,#表示通配任意一個單詞 *表示通配任意多個單詞,例如消費者的RoutingKey爲test.#或#.myRoutingKey都可以獲取RoutingKey爲test.myRoutingKey發送者發送的消息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章