RabbitMQ SpringBoot 消息確認機制 應用

一.概念

1.1消息的確認(Confirm):

是指生產者投遞消息後,如果 Broker 收到消息,則會給我們生產者一個應答。生 產者進行接收應答,用來確定這條消息是否正常的發送到 Broker ,這種方式也是消息的可靠性投遞的核心保障!

Confirm 確認機制流程圖:
在這裏插入圖片描述

1.2 Return 消息機制

用於處理一些不可路 由的消息!

消息生產者,通過指定一個 Exchange 和 Routingkey,把消息送達到某一個隊列中去,然後我們的消費者監聽隊列,進行消費處理操作!但是在某些情況下,如果我們在發送消息的時候,當前的 exchange 不存在或者指定的路由 key 路由不到,這個時候如果我們需要監聽這種不可達的消息,就要使用 Return !

Return 消息機制流程圖:
在這裏插入圖片描述

二.rabbitmq-produce的改動

項目使用上一篇中的項目 rabbitmq-produce、rabbitmq-consumer

2.1 修改配置文件application.yml

代碼如下:

server:
  port: 8783

spring:
  #配置程序名爲rabbitmq-produce-learn
  application:
    name: rabbitmq-produce-learn
  #配置rabbitMq 服務器
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    #虛擬host 可以不設置,使用server默認host
    #virtual-host: JCcccHost
    #支持發佈確認
    publisher-confirms: true
    #支持發佈返回
    publisher-returns: true
    template:
      mandatory: true
    listener:
      direct:
        prefetch: 0
        #採用手動應答
        acknowledge-mode: manual
      simple:
        prefetch: 0  #RabbitMQ 將消息順序發送給多個消費者有兩種模式(公平分發、輪詢模式),
                     #區別在於公平分發的prefetch默認是1,如果設置爲0就是輪詢模式。
        #確認模式
        #採用手動應答
        #none:不確認,不會發送任何ack
        #manual:手動確認,發送端和客戶端都需要手動確認
        #auto:自動確認,就是自動發ack,除非拋異常。
        acknowledge-mode: manual

2.2 在rabbitmq-produce中,新增一個RabbitComfirmAndReturnConfig配置類

RabbitComfirmAndReturnConfig的代碼如下:

package com.example.rabbitmqproduce.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * rabbitmq
 * comfirm 消息確認機制  和 Return 消息機制  配置類
 * 消息的確認,是指生產者投遞消息後,如果 Broker 收到消息,則會給我們生產者一個應答。生
 * 產者進行接收應答,用來確定這條消息是否正常的發送到 Broker ,這種方式也是消息的可靠性投遞的核心保障!
 *
 *
 * Return 消息機制 用於處理一些不可路 由的消息!
 *消息生產者,通過指定一個 Exchange 和 Routingkey,把消息送達到某一個隊列中去,然後我們的消費者監聽隊列,進行消費處理操作!
 *但是在某些情況下,如果我們在發送消息的時候,當前的 exchange 不存在或者指定的路由 key 路由不到,這個時候如果我們需要監聽這種不可達的消息,就要使用 Return !
 *在基礎API中有一個關鍵的配置項:Mandatory:如果爲 true,則監聽器會接收到路由不可達的消息,然後進行後續處理,如果爲 false,那麼 broker 端自動刪除該消息!
 */
@Configuration
public class RabbitComfirmAndReturnConfig {

    private Logger logger = LoggerFactory.getLogger(RabbitComfirmAndReturnConfig.class);

    @Autowired
    private CachingConnectionFactory connectionFactory;

    public final static String ackQueueName = "ackQueue";

    public static final String ackExchangeName = "ackExchange";

    private static final String ackBindingKey = "ackRouting";

    @Bean
    public Queue ackQueue() {
        return new Queue(ackQueueName);
    }


    @Bean
    public DirectExchange ackExchange() {
        return new DirectExchange(ackExchangeName);
    }

    @Bean
    public Binding bindingExchangeAckMessage() {
        return BindingBuilder.bind(ackQueue()).to(ackExchange()).with(ackBindingKey);
    }


    /**
     * 定製化 RabbitTemplate 模版
     * @return
     */
    @Bean
    public RabbitTemplate rabbitTemplate() {

        //若使用confirm-callback或return-callback,必須在配置文件yml 配置publisherConfirms或publisherReturns爲true
        connectionFactory.setPublisherConfirms(true);
        connectionFactory.setPublisherReturns(true);
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);

        //必須設置爲 true,不然當 發送到交換器成功,但是沒有匹配的隊列,不會觸發 ReturnCallback 回調
        //而且 ReturnCallback 比 ConfirmCallback 先回調,意思就是 ReturnCallback 執行完了纔會執行 ConfirmCallback
        rabbitTemplate.setMandatory(true);

        //設置 ConfirmCallback 回調   配置文件yml需要配置 publisher-confirms: true
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            // 如果發送到交換器都沒有成功(比如說刪除了交換器),ack 返回值爲 false
            // 如果發送到交換器成功,但是沒有匹配的隊列(比如說取消了綁定),ack 返回值爲還是 true (這是一個坑,需要注意)
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                String messageId = correlationData.getId();
                if (ack) {
                    logger.info("confirm:"+messageId);
                    logger.info("消息發送成功:correlationData({}),ack({}),cause({})", correlationData, ack, cause);
                }else {
                    logger.info("confirm:"+messageId);
                    logger.info("消息發送失敗:correlationData({}),ack({}),cause({})", correlationData, ack, cause);
                }
            }
        });

        //設置 ReturnCallback 回調   配置文件yml需要配置 publisher-returns: true
        //如果發送到交換器成功,但是沒有匹配的隊列,就會觸發這個回調
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                logger.info("message:"+message);
                logger.warn("消息丟失:exchange({}),route({}),replyCode({}),replyText({}),message:{}", exchange, routingKey, replyCode, replyText, message);
            }
        });
        return rabbitTemplate;
    }
}

2.3 新建消息生產者DirectExchangeProduce

DirectExchangeProduce的代碼如下:

package com.example.rabbitmqproduce.produce;

import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
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;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * 發送信息確認ack
 * @Component 注入到Spring容器中
 */
@Component
public class RabbitAckProduce {

    //注入一個 RabbitTemplate 來發布消息
    @Autowired
    private RabbitTemplate rabbitTemplate;

    private Logger logger = LoggerFactory.getLogger(RabbitAckProduce.class);

    private static final String ackRouteKey = "ackRouting";

    private static final String ackExchangeName = "ackExchange";

    /**
     * 發送消息
     */
    public void sendMessage() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "hello!亞索 面對疾風吧";
        String createTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        Map<String,Object> map=new HashMap<>();
        map.put("messageId",messageId);
        map.put("messageData",messageData);
        map.put("createTime",createTime);
        //CorrelationData用於confirm機制裏的回調確認
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        //將消息攜帶綁定鍵值:directRouting 發送到交換機directExchange
        rabbitTemplate.convertAndSend(ackExchangeName,ackRouteKey, map,correlationData);
        logger.info("mq消息發送結束==》{}", map.toString());
    }
}

2.3 寫個測試的TestController類

代碼如下:

package com.example.rabbitmqproduce.controller;


import com.example.rabbitmqproduce.produce.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("TestController")
public class TestController {

    private static final Logger logger = LoggerFactory.getLogger(TestController.class);

    @Autowired
    private RabbitMqProduce rabbitMqProduce;

    @Autowired
    private DirectExchangeProduce directExchangeProduce;

    @Autowired
    private FanoutExchangeProduce fanoutExchangeProduce;

    @Autowired
    private TopicExchangeProduce topicExchangeProduce;

    @Autowired
    private RabbitAckProduce rabbitAckProduce;

    /**
     * 測試基本消息模型(簡單隊列)
     */
    @RequestMapping(value = "/testSimpleQueue", method = RequestMethod.POST)
    public void testSimpleQueue() {
        logger.info("測試基本消息模型(簡單隊列)SimpleQueue---開始");
        for (int i = 0; i < 10; i++) {
            rabbitMqProduce.sendMessage();
        }
        logger.info("測試基本消息模型(簡單隊列)SimpleQueue---結束");
    }


    /**
     * 測試 Direct-exchange模式
     */
    @RequestMapping(value = "/directExchangeTest", method = RequestMethod.POST)
    public void directExchangeTest() {
        logger.info("測試 Direct-exchange模式 隊列名爲directQueue---開始");
        for (int i = 0; i < 10; i++) {
            directExchangeProduce.sendMessage();
        }
        logger.info("測試 Direct-exchange模式 隊列名爲directQueue---結束");
    }


    /**
     * 測試 Fanout-exchange模式
     */
    @RequestMapping(value = "/fanoutExchangeTest", method = RequestMethod.POST)
    public void fanoutExchangeTest() {
        logger.info("測試 fanout-exchange模式 隊列名爲fanoutQueue---開始");
        fanoutExchangeProduce.sendMessage();
        logger.info("測試 fanout-exchange模式 隊列名爲fanoutQueue---結束");
    }


    /**
     * 測試 Topic-exchange模式   topicA 和 topicB
     */
    @RequestMapping(value = "/topictExchangeTest", method = RequestMethod.POST)
    public void topictExchangeTest() {
        logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameA---開始");
        topicExchangeProduce.sendMessageTopicA();
        logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameA---結束");

        logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameB---開始");
        topicExchangeProduce.sendMessageTopicB();
        logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameB---結束");
    }


    /**
     * 測試 ack模式
     */
    @RequestMapping(value = "/ackTest", method = RequestMethod.POST)
    public void ackTest() {
        logger.info("測試 ack 隊列名爲ackQueue---開始");
        rabbitAckProduce.sendMessage();
        logger.info("測試 ack 隊列名爲ackQueue---結束");
    }

}

三.rabbitmq-consumer的改動

3.1 新建消息消費者RabbitAckConsumer類

RabbitAckConsumer代碼如下:

package com.example.rabbitmqconsumer.consumer;

import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;

/**
 * 收到信息確認ack
 * @RabbitListener(queues = "ackQueue") 監聽名爲ackQueue的隊列
 */

@Component
@RabbitListener(queues = "ackQueue")
public class RabbitAckConsumer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private Logger logger = LoggerFactory.getLogger(RabbitAckConsumer.class);

    /**
     * 消費消息
     * @RabbitHandler 代表此方法爲接受到消息後的處理方法
     */
    @RabbitHandler
    public void receiveMessage(Map msg , Message message , Channel channel) throws IOException {
        // 採用手動應答模式, 手動確認應答更爲安全穩定
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
        logger.info("接收到的消息:---->" + msg.toString());
    }

}

四.測試

首先啓動生產者rabbitmq-produce項目。在postman或瀏覽器上訪問:
http://localhost:8783/TestController/ackTest POST請求
這時可以在rabbitmq-produce的控制檯可以看到
在這裏插入圖片描述

然後再啓動消費者rabbitmq-consumer工程,在rabbitmq-consumer可以看到:
在這裏插入圖片描述

下一章,學習 消費端限流、TTL、死信隊列

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