spring boot整合RabbitMQ详解;消息的确认机制,发送确认(ConfirmCallback, ReturnsCallback),消费手动确认(ACK)

简介

什么叫消息队列?

消息(Message)是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入对象。

消息队列(Message Queue)是一种应用间的通信方式,消息发送后可以立即返回,由消息系统来确保消息的可靠传递。消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管从 MQ 中取消息而不管是谁发布的。这样发布者和使用者都不用知道对方的存在。

消息队列的应用场景

可以看出消息队列是一种应用间的异步协作机制,那什么时候需要使用 MQ 呢?

以常见的订单系统为例,用户点击【下单】按钮之后的业务逻辑可能包括:扣减库存、生成相应单据、发红包、发短信通知。在业务发展初期这些逻辑可能放在一起同步执行,随着业务的发展订单量增长,需要提升系统服务的性能,这时可以将一些不需要立即生效的操作拆分出来异步执行,比如发放红包、发短信通知等。这种场景下就可以用 MQ ,在下单的主流程(比如扣减库存、生成相应单据)完成之后发送一条消息到 MQ 让主流程快速完结,而由另外的单独线程拉取MQ的消息(或者由 MQ 推送消息),当发现 MQ 中有发红包或发短信之类的消息时,执行相应的业务逻辑。

以上是用于业务解耦的情况,其它常见场景包括最终一致性、广播、错峰流控等等。

RabbitMQ 特点

RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。

AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。

RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

RabbitMQ 基本概念

RabbitMQ 内部结构
image

1、Message
消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
2、Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
3、Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
4、Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
5、Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
6、Connection
网络连接,比如一个TCP连接。
7、Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
8、Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
9、Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
10、Broker
表示消息队列服务器实体。

Exchange 类型

Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。
headers 匹配 AMQP 消息的 header 而不是路由键,此外 headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了
下面的代码中也不会编写 headers类型的代码

Direct Exchange (直连型交换机)

直连型交换机,根据消息携带的路由键将消息投递给对应队列。

大致流程,有一个队列绑定到一个直连交换机上,同时赋予一个路由键 routing key 。
然后当一个消息携带着路由值为X,这个消息通过生产者发送给交换机时,交换机就会根据这个路由值X去寻找绑定值也是X的队列。
image

Fanout Exchange(扇型交换机)

扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。
image

Topic Exchange(主题交换机)

主题交换机,这个交换机其实跟直连交换机流程差不多,但是它的特点就是在它的路由键和绑定键之间是有规则的。
简单地介绍下规则:

*  (星号) 用来表示一个单词 (必须出现的)
#  (井号) 用来表示任意数量(零个或多个)单词
通配的绑定键是跟队列进行绑定的,举个小例子
队列Q1 绑定键为 *.TT.*          队列Q2绑定键为  TT.#
如果一条消息携带的路由键为 A.TT.B,那么队列Q1将会收到;
如果一条消息携带的路由键为TT.AA.BB,那么队列Q2将会收到;

image

RabbitMQ安装

一、这是RabbitMQ的官网地址(https://www.rabbitmq.com/install-windows.html) 可以按照官网的文档进行安装
ps:看了下官方文档rabbitMQ是用Erlang 语言开发的,所以需要先安装Erlang 语言。
二、docker-compose安装
建议通过docker进行安装,毕竟用docker命令安装很方便,下面就是我用docker-compose安装rabbitmq的yml文件配置:

version: '3'
services:
  my_rabbitMQ:
    image: "rabbitmq:3.8.3-management"
    container_name: my_rabbitMQ
    restart: always
    privileged: true
    ports:
      - "15672:15672"
      - "5672:5672"
    environment: 
     - "RABBITMQ_DEFAULT_USER=rabbitMQ"
     - "RABBITMQ_DEFAULT_PASS=rabbitMQ"

docker-compose 安装rabbitMQ命令

docker-compose -f rabbitMq-docker-compose.yml up -d

1、docker-compose安装成功后
2、浏览器访问 http://127.0.0.1:15672/ ,并输入账号/密码 rabbitMQ/rabbitMQ 能正常访问rabbitMQ管理界面,就代表安装完成。

image
image
image

spring boot集成RabbitMQ(编码)

项目有两个rabbit-provider(生产者)和rabbit-consumer(消费者)

集成rabbitMQ 主要需要依赖spring-boot-starter-amqp;
java-testdata-generator 是我在gitee上看到 随机测试数据生成器,包括身份证号码,银行卡号,姓名,汉字、手机号,电子邮箱地址和生成insert sql参数列表字符串等的工具包。主要是用来不让测试数据看起来那么单调;

maven依赖
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.4</version>
        </dependency>

        <!--测试接口 添加swagger start-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.6.1</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.6.1</version>
        </dependency>
        <!--测试接口 添加swagger end-->

        <!--Java实现的各种随机测试数据生成器,包括身份证号码,银行卡号,姓名,汉字、手机号 start-->
        <dependency>
            <groupId>com.github.binarywang</groupId>
            <artifactId>java-testdata-generator</artifactId>
            <version>1.1.2</version>
        </dependency>
        <!--Java实现的各种随机测试数据生成器,包括身份证号码,银行卡号,姓名,汉字、手机号 end-->
application.yml配置
server:
  port: 8999
spring:
  #项目名称
  application:
    name: rabbitmq-provider
  #配置rabbitMq 服务器
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: rabbitMQ
    password: rabbitMQ
#    #虚拟host 可以不设置,使用server默认host
#    virtual-host: xkq
    #确认消息已发送到交换机(Exchange)
#    publisher-confirm-type: SIMPLE
    publisher-confirm-type: CORRELATED
    #确认消息已发送到队列(Queue)
    publisher-returns: true

Direct Exchange (直连型交换机)

项目rabbit-provider(生产者)
DirectRabbitConfig配置
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DirectRabbitConfig {

    public static final String TestDirectQueue = "TestDirectQueue";
    public static final String TestDirectExchange = "TestDirectExchange";
    public static final String TestDirectRouting = "TestDirectRouting";

    //队列 起名:TestDirectQueue
    @Bean
    public Queue TestDirectQueue() {
        // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
        //   return new Queue("TestDirectQueue",true,true,false);

        //一般设置一下队列的持久化就好,其余两个就是默认false
        return new Queue(TestDirectQueue,true);
    }

    //Direct交换机 起名:TestDirectExchange
    @Bean
    DirectExchange TestDirectExchange() {
        //  return new DirectExchange("TestDirectExchange",true,true);
        return new DirectExchange(TestDirectExchange,true,false);
    }

    //绑定  将队列和交换机绑定, 并设置用于匹配键:TestDirectRouting
    @Bean
    Binding bindingDirect() {
        return BindingBuilder.bind(TestDirectQueue())
                .to(TestDirectExchange()).with(TestDirectRouting);
    }

}

SendDirectMessageController业务发送消息
import cn.binarywang.tools.generator.ChineseAddressGenerator;
import cn.binarywang.tools.generator.ChineseNameGenerator;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.example.rabbitmqprovider.direct.config.DirectRabbitConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;


@RestController
public class SendDirectMessageController {

    @Autowired
    RabbitTemplate rabbitTemplate;  //使用RabbitTemplate,这提供了接收/发送等等方法

    @ResponseBody
    @GetMapping("/sendDirectMessage")
    public Object sendDirectMessage() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = StrUtil.format("hello 我叫:{} 我住在:{}", ChineseNameGenerator.getInstance().generate(),
                ChineseAddressGenerator.getInstance()
                        .generate());
        Map<String,Object> map = new HashMap<>();
        map.put("messageId",messageId);
        map.put("messageData",messageData);
        map.put("createTime", DateUtil.now());
        //将消息携带绑定键值:TestDirectRouting 发送到交换机TestDirectExchange
        rabbitTemplate.convertAndSend(DirectRabbitConfig.TestDirectExchange, DirectRabbitConfig.TestDirectRouting, map);
        return map;
    }

}
项目rabbit-consumer(消费者)
DirectReceiver接收消息
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
public class DirectReceiver {
    public static final String TestDirectQueue = "TestDirectQueue";
    public static final String TestDirectExchange = "TestDirectExchange";
    public static final String TestDirectRouting = "TestDirectRouting";

    @RabbitListener(queues = DirectReceiver.TestDirectQueue)
    @RabbitHandler
    public void receiver(@Payload HashMap message) {
        log.info("receiver 消费者1号 收到消息  --- message:{}" ,message);
    }

    @RabbitListener(queues = TestDirectQueue)
    @RabbitHandler
    public void receiver2(Map testMessage) {
        log.info("receiver2 消费者2号 收到消息 getClass:{}  ---  {}" ,testMessage.getClass(), testMessage);
    }
}

测试Direct Exchange (直连型交换机)

正常调用接口【http://127.0.0.1:8999/sendDirectMessage 】 ;并且rabbitMQ管理界面看到TestDirectQueue队列有消息新增,代表消息发送成功;
image
image
image

看到控制台日志输出如下,代表消费者 成功接收到推送的消息;多个消费者的情况下 默认是采用轮询的方式进行消费。
image

Fanout Exchange(扇型交换机)
项目rabbit-provider(生产者)
FanoutRabbitConfig配置
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FanoutRabbitConfig {

    public final static String fanoutExchange = "fanoutExchange";
    public static final String fanout_A = "fanout.A";
    public static final String fanout_B = "fanout.B";
    public static final String fanout_C = "fanout.C";
    /**
     *  创建三个队列 :fanout.A   fanout.B  fanout.C
     *  将三个队列都绑定在交换机 fanoutExchange 上
     *  因为是扇型交换机, 路由键无需配置,配置也不起作用
     */
    @Bean
    public Queue queueA() {
        return new Queue(fanout_A);
    }

    @Bean
    public Queue queueB() {
        return new Queue(fanout_B);
    }

    @Bean
    public Queue queueC() {
        return new Queue(fanout_C);
    }

    @Bean
    FanoutExchange fanoutExchange() {
        return new FanoutExchange(fanoutExchange);
    }

    @Bean
    Binding bindingExchangeA() {
        return BindingBuilder.bind(queueA()).to(fanoutExchange());
    }

    @Bean
    Binding bindingExchangeB() {
        return BindingBuilder.bind(queueB()).to(fanoutExchange());
    }

    @Bean
    Binding bindingExchangeC() {
        return BindingBuilder.bind(queueC()).to(fanoutExchange());
    }
}
SendFanoutMessageController业务发送消息
import cn.hutool.core.date.DateUtil;
import com.example.rabbitmqprovider.direct.config.FanoutRabbitConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * 扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。
 * 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。
 */
@RestController
public class SendFanoutMessageController {

    @Autowired
    RabbitTemplate rabbitTemplate;  //使用RabbitTemplate,这提供了接收/发送等等方法

    @GetMapping("/sendFanoutMessage")
    public String sendFanoutMessage() {
        String messageData = "FanoutMessage routingKey is null";
        Map<String, Object> map = new HashMap<>();
        map.put("createTime", DateUtil.now());
        map.put("messageData", messageData);
        rabbitTemplate.convertAndSend(FanoutRabbitConfig.fanoutExchange,null, map);
        return "ok";
    }
    @GetMapping("/sendFanoutMessage1")
    public String sendFanoutMessage1() {
        String messageData = "FanoutMessage routingKey is 'xxx'";
        Map<String, Object> map = new HashMap<>();
        map.put("createTime", DateUtil.now());
        map.put("messageData", messageData);
        //扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个接口绑定一下路由键
        rabbitTemplate.convertAndSend(FanoutRabbitConfig.fanoutExchange,"xxx", map);
        return "ok";
    }
}
项目rabbit-consumer(消费者)
FanoutReceiver接收消息
import java.util.Map;

@Slf4j
@Component
public class FanoutReceiver {
    public static final String fanout_A = "fanout.A";
    public static final String fanout_B = "fanout.B";
    public static final String fanout_C = "fanout.C";

    @RabbitListener(queues = fanout_A)
    @RabbitHandler
    public void fanout_A(Map testMessage) {
        log.info("fanout_A  {}" , testMessage);
    }

    @RabbitListener(queues = fanout_B)
    @RabbitHandler
    public void fanout_B(Map testMessage) {
        log.info("fanout_B  {}" , testMessage);
    }

    @RabbitListener(queues = fanout_C)
    @RabbitHandler
    public void fanout_C(Map testMessage) {
        log.info("fanout_C  {}" , testMessage);
    }

}

测试 Fanout Exchange(扇型交换机)

先调用http://127.0.0.1:8999/sendFanoutMessage ,在调用接口 http://127.0.0.1:8999/sendFanoutMessage1
日志输出如下,可以看到 扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。
这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。
image

Topic Exchange(主题交换机)
项目rabbit-provider(生产者)
TopicRabbitConfig配置
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class TopicRabbitConfig {
    //绑定键
    public final static String man = "topic.man";
    public final static String woman = "topic.woman";
    public final static String xxx = "xxx";
    public final static String topicExchange = "topicExchange";

    @Bean
    public Queue firstQueue() {
        return new Queue(TopicRabbitConfig.man);
    }

    @Bean
    public Queue secondQueue() {
        return new Queue(TopicRabbitConfig.woman);
    }

    @Bean
    public Queue thirdQueue() {
        return new Queue(TopicRabbitConfig.xxx);
    }

    @Bean
    TopicExchange exchange() {
        return new TopicExchange(topicExchange);
    }


    //将firstQueue和topicExchange绑定,而且绑定的键值为topic.man
    //这样只要是消息携带的路由键是topic.man,才会分发到该队列
    @Bean
    Binding bindingExchangeMessage() {
        return BindingBuilder.bind(firstQueue()).to(exchange()).with(man);
    }

    //将secondQueue和topicExchange绑定,而且绑定的键值为用上通配路由键规则topic.#
    // 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列
    @Bean
    Binding bindingExchangeMessage2() {
        return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#");
    }

    @Bean
    Binding bindingExchangeMessage3() {
        return BindingBuilder.bind(thirdQueue()).to(exchange()).with("#");
    }

}
SendTopicMessageController业务发送消息
import com.example.rabbitmqprovider.direct.config.TopicRabbitConfig;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;


@RestController
public class SendTopicMessageController {

    @Autowired
    RabbitTemplate rabbitTemplate;  //使用RabbitTemplate,这提供了接收/发送等等方法


    @ResponseBody
    @GetMapping("/sendTopicMessage1")
    public Object sendTopicMessage1() {
        String messageData = "message: M A N ";
        Map<String, Object> manMap = new HashMap<>();
        manMap.put("messageData", messageData);
        rabbitTemplate.convertAndSend(TopicRabbitConfig.topicExchange, "topic.man", manMap);
        return "ok";
    }

    @ResponseBody
    @GetMapping("/sendTopicMessage2")
    public Object sendTopicMessage2() {
        String messageData = "message: woman is all ";
        Map<String, Object> womanMap = new HashMap<>();
        womanMap.put("messageData", messageData);
        rabbitTemplate.convertAndSend(TopicRabbitConfig.topicExchange, "topic.woman", womanMap);
        return "ok";
    }

    @ResponseBody
    @GetMapping("/sendTopicMessage3")
    public Object sendTopicMessage3() {
        String messageData = "message: xxx ";
        Map<String, Object> womanMap = new HashMap<>();
        womanMap.put("messageData", messageData);
        //routingKey 设置'abc';xxx队列 routingKey配置了# 看能否收到消息
        rabbitTemplate.convertAndSend(TopicRabbitConfig.topicExchange, "abc", womanMap);
        return "ok";
    }

}
项目rabbit-consumer(消费者)
TopicReceiver接收消息
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Map;

@Slf4j
@Component
public class TopicReceiver {
    public static final String topic_man = "topic.man";
    public static final String topic_woman = "topic.woman";
    public final static String xxx = "xxx";

    @RabbitListener(queues = topic_man)
    @RabbitHandler
    public void topic_man(Map testMessage) {
        log.info("我是队列{} 收到消息:{}" ,topic_man, testMessage);
    }

    @RabbitListener(queues = topic_woman)
    @RabbitHandler
    public void topic_woman(Map testMessage) {
        log.info("我是队列{} 收到消息:{}" ,topic_woman, testMessage);
    }

    @RabbitListener(queues = xxx)
    @RabbitHandler
    public void xxx(Map testMessage) {
        log.info("我是队列{} 收到消息:{}" ,xxx, testMessage);
    }

}

测试Topic Exchange(主题交换机)
  1. 调用sendTopicMessage1接口
    routingKey设置的是"topic.man",所以三个队列都收到消息;
    image
  2. 调用sendTopicMessage2接口
    routingKey设置的是"topic.woman",“topic.man”队列设置的routingKey是“topic.man”所以没收到消息;
    image
  3. 调用sendTopicMessage3接口
    routingKey设置的是"abc",队列“xxx”配置的routingKey是“#”,所以只有队列“xxx”收到了消息
    image

消息可靠性

rabbitmq 的消息确认分为两部分:发送消息确认 和 消息接收确认。

项目rabbit-provider(生产者)消息发送确认

发送消息确认:用来确认生产者 producer 将消息发送到 broker ,broker 上的交换机 exchange 再投递给队列 queue的过程中,消息是否成功投递。

消息从 producer 到 rabbitmq broker有一个 confirmCallback 确认模式。

消息从 exchange 到 queue 投递失败有一个 returnCallback 退回模式。

我们可以利用这两个Callback来确保消的100%送达。

1、 ConfirmCallback确认模式

  • ConfirmCallback机制只确认消息是否到达exchange(交换器),不保证消息可以路由到正确的queue;
  • 配置参数需要设置:publisher-confirm-type: CORRELATED;springboot版本较低的话参数设置改成:publisher-confirms: true

2、 ReturnCallback 退回模式

  • ReturnsCallback 消息机制用于处理一个不可路由的消息。在某些情况下,如果我们在发送消息的时候,当前的 exchange 不存在或者指定路由 key 路由不到,这个时候我们需要监听这种不可达的消息

  • 配置参数需要设置:publisher-returns: true

  • 配置参数在application.yml文件(publisher-confirm-type、publisher-returns)

消息发送确认配置如下:

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class RabbitConfig {

    @Bean
    public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);
       // Mandatory为true时,消息通过交换器无法匹配到队列会返回给生产者,为false时匹配不到会直接被丢弃
        rabbitTemplate.setMandatory(true);

        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             *  ConfirmCallback机制只确认消息是否到达exchange(交换器),不保证消息可以路由到正确的queue;
             *  需要设置:publisher-confirm-type: CORRELATED;
             *  springboot版本较低 参数设置改成:publisher-confirms: true
             *
             *  以实现方法confirm中ack属性为标准,true到达
             *  config : 需要开启rabbitmq得ack publisher-confirm-type
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                log.info("ConfirmCallback  确认结果 (true代表发送成功) : {}  消息唯一标识 : {} 失败原因 :{}",ack,correlationData,cause);
            }
        });

        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            /**
             *  ReturnsCallback 消息机制用于处理一个不可路由的消息。在某些情况下,如果我们在发送消息的时候,当前的 exchange 不存在或者指定路由 key 路由不到,这个时候我们需要监听这种不可达的消息
             *   就需要这种return机制
             *
             *  config : 需要开启rabbitmq发送失败回退; publisher-returns 或rabbitTemplate.setMandatory(true); 设置为true
             */
            @Override
            public void returnedMessage(ReturnedMessage returned) {
//                实现接口ReturnCallback,重写 returnedMessage() 方法,
//                方法有五个参数
//                message(消息体)、
//                replyCode(响应code)、
//                replyText(响应内容)、
//                exchange(交换机)、
//                routingKey(队列)。

                log.info("ReturnsCallback    returned : {}",returned);
            }
        });

        return rabbitTemplate;
    }

}

消息发送确认测试接口:

import cn.binarywang.tools.generator.ChineseAddressGenerator;
import cn.binarywang.tools.generator.ChineseNameGenerator;
import cn.hutool.core.util.StrUtil;
import com.example.rabbitmqprovider.direct.config.DirectRabbitConfig;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;


@RestController
public class SendCallbackMessageController {

    @Autowired
    RabbitTemplate rabbitTemplate;  //使用RabbitTemplate,这提供了接收/发送等等方法

    @ResponseBody
    @GetMapping("/sendMessageToExchangeFail")
    public Object sendMessageToExchangeFail() {
        String messageData = StrUtil.format("hello 我叫:{} 我住在:{}", ChineseNameGenerator.getInstance().generate(),
                ChineseAddressGenerator.getInstance()
                        .generate());
        Map<String,Object> map = new HashMap<>();
        map.put("messageData",messageData);
        //发送一个消息到 不存在到exchange
        rabbitTemplate.convertAndSend(DirectRabbitConfig.TestDirectExchange.concat("test"), DirectRabbitConfig.TestDirectRouting, map,new CorrelationData(UUID.randomUUID().toString()));
        return map;
    }
    @ResponseBody
    @GetMapping("/sendMessageToQueueFail")
    public Object sendMessageToQueueFail() {
        String messageData = StrUtil.format("hello 我叫:{} 我住在:{}", ChineseNameGenerator.getInstance().generate(),
                ChineseAddressGenerator.getInstance()
                        .generate());
        Map<String,Object> map = new HashMap<>();
        map.put("messageData",messageData);
        //发送一个消息到 不存的队列里;
        rabbitTemplate.convertAndSend(DirectRabbitConfig.TestDirectExchange, "xxx", map,new CorrelationData(UUID.randomUUID().toString()));
        return map;
    }
}

消息发送确认测试

  1. 调用sendMessageToExchangeFail接口
2022-03-06 20:31:00.488 ERROR 32087 --- [ 127.0.0.1:5672] o.s.a.r.c.CachingConnectionFactory       : Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'TestDirectExchangetest' in vhost '/', class-id=60, method-id=40)
2022-03-06 20:31:00.489  INFO 32087 --- [nectionFactory2] c.e.r.direct.config.RabbitConfig         : ConfirmCallback  确认结果 (true代表发送成功) : false  消息唯一标识 : CorrelationData [id=5aaf1d44-85cc-42ae-8ae5-cbca5074cefd] 失败原因 :channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'TestDirectExchangetest' in vhost '/', class-id=60, method-id=40)
  1. 调用sendMessageToQueueFail接口
2022-03-06 20:31:30.186  INFO 32087 --- [nectionFactory2] c.e.r.direct.config.RabbitConfig         : ReturnsCallback    returned : ReturnedMessage [message=(Body:'[serialized object]' MessageProperties [headers={spring_returned_message_correlation=8699b6b8-0b12-420f-b866-73f54ba0a002}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0]), replyCode=312, replyText=NO_ROUTE, exchange=TestDirectExchange, routingKey=xxx]
2022-03-06 20:31:30.186  INFO 32087 --- [nectionFactory1] c.e.r.direct.config.RabbitConfig         : ConfirmCallback  确认结果 (true代表发送成功) : true  消息唯一标识 : CorrelationData [id=8699b6b8-0b12-420f-b866-73f54ba0a002] 失败原因 :null

项目rabbit-consumer(消费者)消息接收确认

消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK

消息确认模式有:
  • AcknowledgeMode.NONE:自动确认
  • AcknowledgeMode.AUTO:根据情况确认(默认值)
  • AcknowledgeMode.MANUAL:手动确认
手动确认消息
1、basicAck

表示成功确认,使用此回执方法后,消息会被rabbitmq broker 删除。

void basicAck(long deliveryTag, boolean multiple)
  • deliveryTag:表示消息投递序号,每次消费消息或者消息重新投递后,deliveryTag都会增加。deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel

  • multiple:是否批量确认,值为 true 则会一次性 ack所有小于当前消息 deliveryTag 的消息。

  • 举个栗子: 假设我先发送三条消息deliveryTag分别是5、6、7,可它们都没有被确认,当我发第四条消息此时deliveryTag为8,multiple设置为 true,会将5、6、7、8的消息全部进行确认。

2、basicNack

表示失败确认,一般在消费消息业务异常时用到此方法,可以将消息重新投递入队列。

void basicNack(long deliveryTag, boolean multiple, boolean requeue)
  • deliveryTag:表示消息投递序号。
  • multiple:是否批量确认。
  • requeue:值为 true 消息将重新入队列。
3、basicReject

拒绝消息,与basicNack区别在于不能进行批量操作,其他用法很相似。

void basicReject(long deliveryTag, boolean requeue)
  • deliveryTag:表示消息投递序号。
  • requeue:值为 true 消息将重新入队列。

开启手动确认消息

  • application.yml配置文件开启
    acknowledge-mode设置manual
spring:
  #项目名称
  application:
    name: rabbitmq-custom
  #配置rabbitMq 服务器
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: rabbitMQ
    password: rabbitMQ
    listener:
      simple:
        acknowledge-mode: manual
  • 注解开启手动确认
    @RabbitListener注解中设置参数ackMode= "MANUAL"开启
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.HashMap;

/**
 * 先注释 DirectReceiver的@Component
 * 用TestAckDirectReceiver方法来验证 手动ack
 */
@Slf4j
@Component
public class TestAckDirectReceiver {
    public static final String TestDirectQueue = "TestDirectQueue";
    public static final String TestDirectExchange = "TestDirectExchange";
    public static final String TestDirectRouting = "TestDirectRouting";

    @RabbitListener(queues = TestAckDirectReceiver.TestDirectQueue,
            ackMode= "MANUAL")
    @RabbitHandler
    public void receiver(@Payload HashMap dataMsg, Channel channel, Message message) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        log.info("消费者1号 deliveryTag:{} dataMsg:{} ",deliveryTag ,dataMsg);
        try {
           int i = 1/0;
        } catch (Exception e) {
            log.error("MyAckReceiver error : {}  deliveryTag:{}",e.getMessage(),deliveryTag);
            channel.basicReject(deliveryTag, true);
        }

    }

    @RabbitListener(queues = TestAckDirectReceiver.TestDirectQueue,
            ackMode= "MANUAL")
    @RabbitHandler
    public void receiver1(@Payload HashMap dataMsg, Channel channel, Message message) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        log.info("消费者2号 deliveryTag:{} dataMsg:{} ",deliveryTag ,dataMsg);
        channel.basicAck(deliveryTag,true);
    }

}

手动消息确认消费测试

可以看到消费者1号调用basicReject方法拒绝了消息;消费者2号调用channel.basicAck方法手动确认了消费

2022-03-06 21:23:16.816  INFO 51530 --- [ntContainer#4-1] c.e.c.direct.TestAckDirectReceiver       : 消费者1号 deliveryTag:5 dataMsg:{createTime=2022-03-06 21:23:16, messageId=33ae8439-4635-4499-ae7d-c167f100c26a, messageData=hello 我叫:毛儿 我住在:湖南省湘西土家族苗族自治州露劲路7725号纯辟顽小区8单元1122室} 
2022-03-06 21:23:16.816 ERROR 51530 --- [ntContainer#4-1] c.e.c.direct.TestAckDirectReceiver       : MyAckReceiver error : / by zero  deliveryTag:5
2022-03-06 21:23:16.818  INFO 51530 --- [ntContainer#3-1] c.e.c.direct.TestAckDirectReceiver       : 消费者2号 deliveryTag:6 dataMsg:{createTime=2022-03-06 21:23:16, messageId=33ae8439-4635-4499-ae7d-c167f100c26a, messageData=hello 我叫:毛儿 我住在:湖南省湘西土家族苗族自治州露劲路7725号纯辟顽小区8单元1122室} 

代码我已经上传到gitee 代码传送门

参考资料:
https://www.jianshu.com/p/79ca08116d57
https://blog.csdn.net/qq_35387940/article/details/100514134
https://blog.csdn.net/weixin_32820639/article/details/111240447
https://www.cnblogs.com/gyjx2016/p/13705307.html
https://zhuanlan.zhihu.com/p/152325703

posted @ 2022-03-06 21:46  _否极泰来  阅读(3287)  评论(0编辑  收藏  举报