04-RabbitMQ常用的六種模型以及在SpringBoot中的應用

在RabbitMQ中,我們常用的模型主要有六種,分別是:

  • Hello World
  • Work queues
  • Publish/Subscribe
  • Routing
  • Topic
  • RPC

俗話說得好,光說不練假把式,下面我們結合springBoot逐一實現這六種模型。

Hello World

從上圖可以看出,這是一個默認交換機的單播路由,並且每個隊列只有一個消費者。

Work queues

從上圖可以看出,主要的部分是:默認交換機的單播路由,並且每個隊列有多個消費者。

Publish/Subscribe

從上圖可以看出,主要的部分是:扇形交換機的多播路由。

Routing

從上圖可以看出,主要的部分是:直連交換機的多播路由。

Topic

從上圖可以看出,主要的部分是:主題交換機的多播路由。

RPC

從上圖可以看出,主要的部分是:默認交換機的單播路由。

環境

  • 下面我們代碼演示一下除了RPC之外的其他五種模型,在SpringBoot中的用法

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.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.qbz</groupId>
    <artifactId>rabbit-mq-test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rabbit-mq-test</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-amqp</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

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

</project>

application.yml

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    virtual-host: /qbz-test #虛擬主機,必須存在,不然會報錯。
    username: guest
    password: guest
    publisher-confirms: true #消息發送到交換機確認機制,是否確認回調,默認false
    publisher-returns: true #消息發送到交換機確認機制,是否返回回調,默認false
    listener:
      simple:
        prefetch: 1 #預先載入數量,默認值250
        concurrency: 10 #指定最小消費數量
        max-concurrency: 20 #指定最大的消費者數量,當並行消費者數量到達concurrency後會開至最大max-concurrency
        acknowledge-mode: manual # 採用手動應答,設置爲手動應答後,消費者如果不進行手動應答,會處於假死狀態,不能再消費。默認auto
        retry:
          enabled: true # 失敗重試機制,默認爲false.
          max-attempts: 1 # 失敗後,再重試幾次,默認爲:3

RabbitMqTestApplication.java

package cn.qbz.rabbitmqtest;

import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * RabbitMQ 測試
 *
 * @author qubianzhong
 * @Date 13:42 2019/7/26
 */
@SpringBootApplication
@EnableRabbit //新增開啓Rabbit註解
public class RabbitMqTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(RabbitMqTestApplication.class, args);
    }

}

RabbitMqProduceController.java

package cn.qbz.rabbitmqtest.controller;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author qubianzhong
 * @date 2019/7/22 13:51
 */
@RestController
public class RabbitMqProduceController {

    @Autowired
    private AmqpTemplate amqpTemplate;
    @Autowired
    private AmqpAdmin amqpAdmin;

    @GetMapping(value = "/produce")
    public String produceMsg(@RequestParam(value = "msg") String msg) {
        /****************************hello/work start****************************
         * hello-world、work queues 均是隻綁定隊列
         * 發佈消息時,如下所示,推薦使用第二種
         */
        //1.如果隊列不存在,則消息會進入“黑洞”
        amqpTemplate.convertAndSend("hello", msg + System.currentTimeMillis());
        /**
         * 2.如果隊列不存在,則進行創建,如果隊列已存在,則使用此隊列
         * public Queue(String name) {
         *      //The queue is durable, non-exclusive and non auto-delete.
         * 		this(name, true, false, false);
         *  }
         */
        Queue queue = new Queue("hello");
        amqpAdmin.declareQueue(queue);
        amqpTemplate.convertAndSend(queue.getName(), msg + System.currentTimeMillis());

        //3.work
        queue = new Queue("work");
        amqpAdmin.declareQueue(queue);
        for (int i = 0; i < 30; i++) {
            amqpTemplate.convertAndSend(queue.getName(), "wwoork" + i);
        }
        /****************************hello/work end****************************/

        /****************************Publish/Subscribe start****************************
         * 生產者只向扇形交換機發送消息,扇形交換機負責向綁定其隊列上的所有消費者進行分發。
         * public AbstractExchange(String name) {
         *      //Construct a new durable, non-auto-delete Exchange with the provided name.
         * 		this(name, true, false);
         * }
         */
        for (int i = 0; i < 5; i++) {
            Exchange pubSubExchange = new FanoutExchange("pub-sub-exchange");
            amqpAdmin.declareExchange(pubSubExchange);
            amqpTemplate.convertAndSend(pubSubExchange.getName(), null, msg + System.currentTimeMillis());
        }
        /****************************Publish/Subscribe end****************************/

        /****************************Routing start****************************
         * Routing  消費者消費的時候,多個路由鍵綁定一個隊列
         */
        Exchange routeExchange = new DirectExchange("routing-exchange");
        amqpAdmin.declareExchange(routeExchange);
        amqpTemplate.convertAndSend(routeExchange.getName(), "routing-log-error", "routing-log-error:" + System.currentTimeMillis());
        amqpTemplate.convertAndSend(routeExchange.getName(), "routing-log-info", "routing-log-info:" + System.currentTimeMillis());
        amqpTemplate.convertAndSend(routeExchange.getName(), "routing-log-waring", "routing-log-waring:" + System.currentTimeMillis());

        /****************************Routing end****************************/

        /****************************Topic start****************************
         * Topic  消費者消費的時候,多個路由鍵,模糊匹配,綁定一個隊列,其實和routing差不多
         */
        Exchange topicExchange = new TopicExchange("topic-exchange");
        amqpAdmin.declareExchange(topicExchange);
        amqpTemplate.convertAndSend(topicExchange.getName(), "topic-log-error.20190706", "topic-log-error.20190706:" + System.currentTimeMillis());
        amqpTemplate.convertAndSend(topicExchange.getName(), "topic-log-info.20190606", "topic-log-info.20190606:" + System.currentTimeMillis());
        amqpTemplate.convertAndSend(topicExchange.getName(), "topic-log-error.20190708", "topic-log-error.20190708:" + System.currentTimeMillis());
        amqpTemplate.convertAndSend(topicExchange.getName(), "topic-log-waring.20190506", "topic-log-waring.20190506:" + System.currentTimeMillis());
        amqpTemplate.convertAndSend(topicExchange.getName(), "topic-log-waring.20190516", "topic-log-waring.20190516:" + System.currentTimeMillis());
        amqpTemplate.convertAndSend(topicExchange.getName(), "topic-log-info.20190723", "topic-log-info.20190723:" + System.currentTimeMillis());

        /****************************Topic end****************************/

        return "success";
    }
}


RabbitTestListener.java

package cn.qbz.rabbitmqtest.rabbit;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * @author qubianzhong
 * @date 2019/7/26 14:40
 */
@Component
public class RabbitTestListener {
    /****************************demo 演示如何消費消息 start****************************
     * 推薦使用第二種或第三種
     * 當不需要綁定交換機的時候,如果使用第三種,exchange置爲空會報錯
     */

    /**
     * 1.此種用法需要手動創建隊列,不然會報錯
     */
    @RabbitListener(queues = "demo-1")
    public void demo1(Message message, Channel channel) throws IOException {
        System.err.println("demo-1:" + new String(message.getBody()));
        //信道上發佈的消息都會被指派一個唯一的ID號:message.getMessageProperties().getDeliveryTag()
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);

        //1:接收到的被指派一個唯一的ID號:deliveryTag;
        // 2:true當前consumer拒絕所有的deliveryTag包括此次的,false當前consumer只拒絕此次的這個deliveryTag;
        // 3:true此消息重新排隊,而不是被丟棄或者扔進死信隊列中
        //channel.basicNack(message.getMessageProperties().getDeliveryTag(), true, true);
    }

    /**
     * 2.如果隊列不存在,則會新建;如果隊列已存在,則使用此隊列
     */
    @RabbitListener(queuesToDeclare = @Queue("demo-2"))
    public void demo2(Message message, Channel channel) throws IOException {
        System.err.println("demo-2:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }


    /**
     * 3.綁定隊列、交換機
     * 如果隊列或交換機不存在,則新建;如果已存在,則使用。
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue("demo-3"),
            exchange = @Exchange("demo-exchange-3")
    ))
    public void demo3(Message message, Channel channel) throws IOException {
        System.err.println("demo-3:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }

    /****************************demo 演示如何消費消息 start****************************/

    /****************************hello start****************************
     * 消費 hello-world
     */
    @RabbitListener(queuesToDeclare = {@Queue("hello")})
    public void hello(Message message, Channel channel) throws IOException {
        System.err.println("hello:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }
    /****************************hello end****************************/

    /****************************work start****************************
     * 消費 work 一個隊列對應多個消費者,此時,消息是平均分配到每個消費者手裏的。
     */
    @RabbitListener(queuesToDeclare = {@Queue("work")})
    public void work1(Message message, Channel channel) throws IOException {
        System.err.println("work1:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }

    @RabbitListener(queuesToDeclare = {@Queue("work")})
    public void work2(Message message, Channel channel) throws InterruptedException, IOException {
        Thread.sleep(1230);
        System.err.println("work2:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }

    @RabbitListener(queuesToDeclare = {@Queue("work")})
    public void work3(Message message, Channel channel) throws InterruptedException, IOException {
        Thread.sleep(30);
        System.err.println("work3:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }
    /****************************work end****************************/


    /****************************Publish/Subscribe start****************************
     * 消費 Publish/Subscribe 扇形交換機負責向綁定其隊列上的所有消費者進行分發。
     * 此處,exchange和queue如果不存在,則會新建。
     * 注意:@Exchange註解屬性 type需要定義爲 fanout,不然會是默認的 direct
     */
    @RabbitListener(bindings = {@QueueBinding(
            value = @Queue("sub-pub-queue1"),
            exchange = @Exchange(value = "pub-sub-exchange", type = ExchangeTypes.FANOUT)
    )})
    public void pubSub1(Message message, Channel channel) throws IOException {
        System.err.println("sub-pub-queue1:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }

    @RabbitListener(bindings = {@QueueBinding(
            value = @Queue("sub-pub-queue2"),
            exchange = @Exchange(value = "pub-sub-exchange", type = ExchangeTypes.FANOUT)
    )})
    public void pubSub2(Message message, Channel channel) throws IOException {
        System.err.println("sub-pub-queue2:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }
    /****************************Publish/Subscribe end****************************/


    /****************************Routing start****************************
     * 消費 Routing
     * 其中一個隊列,只消費error信息,一個隊列消費所有信息
     */
    @RabbitListener(bindings = {@QueueBinding(value = @Queue("routing-log-error"), key = {"routing-log-error"}, exchange = @Exchange(value = "routing-exchange"))})
    public void routing1(Message message, Channel channel) throws IOException {
        System.err.println("routing-log-error:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }

    @RabbitListener(bindings = {@QueueBinding(value = @Queue("routing-log-all"), key = {"routing-log-error", "routing-log-info", "routing-log-waring"}, exchange = @Exchange(value = "routing-exchange")
    )})
    public void routing2(Message message, Channel channel) throws IOException {
        System.err.println("routing-log-all:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }
    /****************************Routing end****************************/


    /****************************Topic start****************************
     * 消費 Topic
     * 其中一個隊列,消費前綴爲'topic-log-info.'的信息,
     * 一個隊列消費前綴爲'topic-log-error.'的和模糊匹配'.201907.'的信息
     */
    @RabbitListener(bindings = {@QueueBinding(value = @Queue("topic-log-1"), key = {"topic-log-info.*"}, exchange = @Exchange(value = "topic-exchange", type = ExchangeTypes.TOPIC))})
    public void topic1(Message message, Channel channel) throws IOException {
        System.err.println("所有的info:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }

    @RabbitListener(bindings = {@QueueBinding(value = @Queue("topic-log-2"), key = {"*.201907.*", "topic-log-error.*"}, exchange = @Exchange(value = "topic-exchange", type = ExchangeTypes.TOPIC))})
    public void topic2(Message message, Channel channel) throws IOException {
        System.err.println("所有的error and 7月份:" + new String(message.getBody()));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
    }
    /****************************Routing end****************************/

}

RabbitTemplateCallback.java

package cn.qbz.rabbitmqtest.rabbit;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author qubianzhong
 * @date 2019/7/31 10:56
 */
@Component
public class RabbitTemplateCallback implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    private RabbitTemplate rabbitTemplate;

    @Autowired
    public void setRabbitTemplate(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
        this.rabbitTemplate.setMandatory(true);
        this.rabbitTemplate.setConfirmCallback(this::confirm);
        this.rabbitTemplate.setReturnCallback(this::returnedMessage);
    }

    /**
     * Confirmation callback.
     *
     * @param correlationData correlation data for the callback.
     * @param ack             true for ack, false for nack
     * @param cause           An optional cause, for nack, when available, otherwise null.
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (!ack) {
            System.err.println("消息未能在交換機上得到確認!異常處理......");
        }
    }

    /**
     * Returned message callback.
     *
     * @param message    the returned message.
     * @param replyCode  the reply code.
     * @param replyText  the reply text.
     * @param exchange   the exchange.
     * @param routingKey the routing key.
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.err.println("消息未能到達隊列!" + "return exchange: " + exchange + ", routingKey: "
                + routingKey + ", replyCode: " + replyCode + ", replyText: " + replyText);
    }
}

RPC 補充

我們並不推薦RPC式的mq調用,這麼做完全沒有發揮mq異步削峯的作用。如果有使用RPC的需求,請移步SpringCloud或者Dubbo。

我們雖然不使用RabbitMQ來進行RPC調用,但是我們也要了解,RabbitMQ爲啥子可以實現RPC。

當使用RabbitMQ來實現RPC時.你只是簡單地發佈消息而已。RabbitMQ會負責使用綁定來路由消息到達合適的隊列。RPC服務器會從這些隊列上消費消息。RabbitMQ替你完成了所有這些艱難的工作:將消息路由到合適的地方,通過多臺RPC服務器對RPC消息進行負載均衡,甚至當處理消息的服務器崩潰時,將RPC消息重發到另一臺。

問題在於。如何將應答返回給客戶端呢?畢竟,到目前爲止你體驗的RabbitMQ是發後即忘模型。

RabbitMQ團隊想出了一個優雅的解決方案:使用消息來發迴應答。在每個AMQP消息頭裏有個字段叫作reply_ to,消息的生產者可以通過該字段來確定隊列名稱,並監聽隊列等待應答。然後接收消息的RPC服務器能夠檢杳reply _to字段,並創建包含應答內容的新的消息,並以隊列名稱作爲路由鍵。

你也許想:“光是每次創建唯一隊列名就得花很多工夫吧。我們怎樣阻止其他客戶端讀到應答消息呢?”

我們前面說過,如果你聲明瞭沒有名字的隊列,RabbitMQ會爲你指定一個。這個名字恰好是唯一的隊列名;同時在聲明的時候指定exclusive參數.確保只有你可以讀取隊列上的消息。所有RPC客戶端需要做的是聲明臨時的、排他的、匿名隊列,並將該隊列名稱包含到RPC消息的reply _to頭中。於是服務器端就知道應答消息該發往哪兒了。值得注意的是我們並沒有提到將應答隊列綁定到交換器上。這是因爲當RPC服務器將應答消息發佈到RabbitMQ而沒有指定交換器時.RabbitMQ就知道目的地是應答隊列,路由鍵就是隊列的名稱。

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