RabbitMQ基础二

消息可靠性

  • 消息从生产者发送到Exchange,再到Queue,再到消费者,有三种消息丢失的可能性:

    • 生产者发送消息时丢失:
      • 生产者发送消息时连接MQ失败
      • 生产者发送的消息到达MQ后未送达Exchange
      • 生产者发送消息到达MQ的Exchange后,未找到合适的Queue
    • MQ宕机导致Queue消息丢失
      • 消息到达MQ并保存到队列后,尚未消费就突然宕机
    • 消费者处理消息时:
      • 消费者接收到消息后未消费就宕机
      • 消费者接收到消息,在处理过程中抛出异常
  • 为保证MQ的(消息)可靠性,解决以上三种消息丢失问题,就需要保证消息传递的每个步骤的可靠性即可,主要从三方面解决:确保生产者一定把消息发送到MQ、确保MQ不会将消息弄丢、确保消费者一定要处理消息

    • 因此,为保证消息可靠性,需学习四个方面:
      • 生产者消息确认
      • 数据持久化
      • 消费者消息确认
      • 消费失败重试机制
  • 注意

    • 生产者消息确认、数据持久化、消费者消息确认三节内容的代码示例均以DirectExchange(路由交换机))示例为基础进行演示 ,系统中交换机(cgrs572.direct)、队列(direct.queue1和direct.queue2)均是已存在状态。因此,若不存在,则需先创建。具体内容可详见RabbitMQ基础部分内容

生产者消息确认

  • 生产者确认机制
    • RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。
    • 消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。
    • 生产者确认机制 结果有两种回执:
      • publisher-confirm:发送确认回执
        • 临时消息成功投递到交换机,则返回 publisher-confirm ACK确认信息
        • 持久消息成功投递到交换机,则返回 publisher-confirm ACK确认信息
        • 消息(临时消息、持久消息)未成功投递到交换机,返回 publisher-confirm NACK确认信息
        • 返回确认信息(发送确认回执)用的是ConfirmCallback
        • 注意:
          1. 只要成功投递到交换机,不论后续是什么样的流程(失败or成功),则一定会返回publisher-confirm ACK确认信息;只要投递到交换机失败,则一定会返回publisher-confirm NACK确认信息
          2. publisher-confirm(发送确认回执) 只关心是否成功投递到交换机。至于成功投递到交换机后,是否路由到Queue不是其关心的问题
      • publisher-return:发送者回执
        • 消息投递到交换机(前提),但没有路由到Queue(关键) (即路由失败,未入队成功),则返回 publisher-confirm ACK确认信息 及 publisher-return 路由失败原因(mandatory=true
        • 消息投递到交换机(前提),并成功路由到Queue(关键) (即入队成功),此时仅返回 publisher-confirm ACK确认信息 , publisher-return不会返回任何信息
        • 返回路由失败原因(发送者回执)用的是ReturnCallback
        • 注意:
          1. 消息未成功投递到交换机,此时只会返回publisher-confirm NACK确认信息,并不会触发publisher-return,因为此时消息根本就没有到路由这一步

SpringAMQP生产者消息确认示例

本示例以 路由交换机示例 为基础进行演示,博主系统中交换机(cgrs572.direct)、队列(direct.queue1和direct.queue2)均是已存在状态。因此,若不存在,则需先创建。具体内容可详见RabbitMQ基础部分内容

前置步骤

  • 在生产者模块(即publisher模块)的application.yml文件中新增配置,代码如下

    yaml 复制代码
    spring:
      rabbitmq:
        publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
        publisher-returns: true # 开启publisher-return机制,同样是基于callback机制,不过定义的是ReturnCallback
        template: # 定义消息路由失败时所采取的策略
          mandatory: true # true:调用ReturnCallback   false:直接丢弃消息

    配置文件完整代码如下:

    yaml 复制代码
    logging:
      pattern:
        dateformat: MM-dd HH:mm:ss:SSS
      level:
        at.guigu: debug
    spring:
      rabbitmq:
        host: 192.168.204.128 # rabbitMQ的公司ip地址 (自用地址192.168.10.100)
        port: 5672 # 配置RabbitMQ的消息发送处理接口(即消息通信接口)
        username: root # 配置RabbitMQ的用户名
        password: 123456 # 配置RabbitMQ的密码
        virtual-host: / # 配置可使用的虚拟主机
        publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
        publisher-returns: true # 开启publisher-return机制,同样是基于callback机制,不过定义的是ReturnCallback
        template: # 定义消息路由失败时所采取的策略
          mandatory: true # true:调用ReturnCallback   false:直接丢弃消息
    • publisher-confirm-type:开启publisher confirm机制,并设置confirm类型,有两种类型
      • simple(不推荐):生产者发送消息后,会同步等待MQ的confirm结果(即回执),直到超时。因此,这种方式又叫做同步阻塞等待,会导致代码的阻塞
      • correlated(推荐):MQ异步回调。也就是说生产者发送消息后不会同步等待,而是会去做其它事情,返回MQ的confirm结果(即回执)会通过callback机制(即回调机制)来完成,即定义ConfirmCallback,MQ返回结果时会回调ConfirmCallback
    • publisher-returns:开启publisher-return功能,同样是基于callback机制(即回调机制),不过定义的是ReturnCallback,MQ返回结果时会回调ReturnCallback
    • template.mandatory:定义消息路由失败时的策略
      • true:路由失败时会通过callback机制(即回调机制)来返回路由失败原因,回调的是ReturnCallback
      • false:直接丢弃消息,并不会通过callback机制(即回调机制)返回路由失败原因

配置回调函数ReturnCallback

目标:配置publisher-return的回调机制所需的回调函数

注意:每个RabbitTemplate只能配置一个ReturnCallback,也就是在配置类中统一设置(此时属于全局配置,都会共用这一个)
解释:

  • RabbitTemplate是由Spring创建的单例Bean,并不是由我们自己创建的,所以RabbitTemplate只有一个。
  • 又因为RabbitTemplate只能配置一个ReturnCallback,所以我们不能在每次发消息时配置(因为在每次发消息时配置的话就会配置多个ReturnCallback
  • 所以需要在项目启动时配置,使其全局生效,也就是在配置类中统一配置

方式一

  • 利用ApplicationContextAware 接口。它是一种 "反向注入机制"
    允许一个 Bean 在初始化时获得 Spring 容器的引用(即ApplicationContext),从而能主动获取其他 Bean 或资源。由此来提前获取RabbitTemplate并配置一个ReturnCallback
  • 此处通过创建一个实现ApplicationContextAware 接口的配置类,并重写其中的setApplicationContext方法来获取到Spring 容器的引用(即ApplicationContext),并通过容器获取RabbitTemplate,进而配置ReturnCallback,以此来实现全局使用一个ReturnCallback

方式二

  • 利用@PostConstruct注解。它是一个生命周期回调注解,用在方法上,让Bean在 创建并完成依赖注入之后、正式被使用之前(对外可用之前) 自动执行一次
  • 由此来实现RabbitTemplate被正式使用前配置一个ReturnCallback

注意:以上两种方式是基于对Spring生命周期的理解,可详见Spring完整知识点一中SPring的生命周期

  • 方式一: 在生产者模块(即publisher)中创建config包,并在该包下创建实现了ApplicationContextAware接口的配置类CommonConfig,代码如下:

    • SpringBoot2.6.x版本之前,使用ReturnCallback接口

      该版本案例Demo已上传至Gitee,可详见Gitee项目messageConfirm/publisherConfirm/ReturnCallback-方式1/SpringBoot2.6.x之前

      java 复制代码
      package at.guigu.mq.config;
      
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.beans.BeansException;
      import org.springframework.context.ApplicationContext;
      import org.springframework.context.ApplicationContextAware;
      import org.springframework.context.annotation.Configuration;
      
      @Slf4j
      @Configuration
      public class CommonConfig implements ApplicationContextAware {
      
          @Override
          public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
              // 获取RabbitTemplate
              RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
              // 设置ReturnsCallback
              rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
                  log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",replyCode, replyText, exchange, routingKey, message.toString());
              });
          }
      }
    • SpringBoot2.6.x版本及该版本之后,使用ReturnsCallback接口

      该版本案例Demo已上传至Gitee,可详见Gitee项目messageConfirm/publisherConfirm/ReturnCallback-方式1/SpringBoot2.6.x之后

      java 复制代码
      package at.guigu.mq.config;
      
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.amqp.core.Message;
      import org.springframework.amqp.core.ReturnedMessage;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.beans.BeansException;
      import org.springframework.context.ApplicationContext;
      import org.springframework.context.ApplicationContextAware;
      import org.springframework.context.annotation.Configuration;
      
      @Slf4j
      @Configuration
      public class CommonConfig implements ApplicationContextAware {
      
          @Override
          public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
              // 获取RabbitTemplate
              RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
              // 设置ReturnsCallback
              rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
                  @Override
                  public void returnedMessage(ReturnedMessage returnedMessage) {
                      int replyCode = returnedMessage.getReplyCode();
                      String replyText = returnedMessage.getReplyText();
                      String exchange = returnedMessage.getExchange();
                      String routingKey = returnedMessage.getRoutingKey();
                      Message message = returnedMessage.getMessage();
                      log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",replyCode, replyText, exchange, routingKey, message.toString());
                  }
              });
              /* Lambda表达式形式
              rabbitTemplate.setReturnsCallback(returnedMessage -> {
                  int replyCode = returnedMessage.getReplyCode();
                  String replyText = returnedMessage.getReplyText();
                  String exchange = returnedMessage.getExchange();
                  String routingKey = returnedMessage.getRoutingKey();
                  Message message = returnedMessage.getMessage();
                  log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",replyCode, replyText, exchange, routingKey, message.toString());
              });*/
          }
      }
  • 方式二: 在生产者模块(即publisher)中创建config包,并在该包下创建配置类CommonConfig,代码如下

    • SpringBoot2.6.x版本之前,使用ReturnCallback接口

      该版本案例Demo已上传至Gitee,可详见Gitee项目messageConfirm/publisherConfirm/ReturnCallback-方式2/SpringBoot2.6.x之前

      java 复制代码
      package at.guigu.mq.config;
      
      import jakarta.annotation.PostConstruct;
      import lombok.RequiredArgsConstructor;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.context.annotation.Configuration;
      
      /**
       * SpringBoot2.6.x版本之前,使用`ReturnCallback`接口配置回调函数
       * 由于博主使用的SpringBoot是3.x,因此才会报错
       */
      @Slf4j
      @Configuration
      @RequiredArgsConstructor
      public class CommonConfig {
      
          private final RabbitTemplate rabbitTemplate;
      
          @PostConstruct
          public void init() {
              // 设置ReturnCallback
              rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
                  log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",replyCode, replyText, exchange, routingKey, message.toString());
              });
          }
      }
    • SpringBoot2.6.x版本及该版本之后,使用ReturnsCallback接口

      该版本案例Demo已上传至Gitee,可详见Gitee项目messageConfirm/publisherConfirm/ReturnCallback-方式2/SpringBoot2.6.x之后

      java 复制代码
      package at.guigu.mq.config;
      
      import jakarta.annotation.PostConstruct;
      import lombok.RequiredArgsConstructor;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.amqp.core.Message;
      import org.springframework.amqp.core.ReturnedMessage;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.context.annotation.Configuration;
      
      /**
       * 方式二
       * SpringBoot2.6.x版本之后,使用`ReturnsCallback`接口配置回调函数
       */
      @Slf4j
      @Configuration
      @RequiredArgsConstructor
      public class CommonConfig {
      
          private final RabbitTemplate rabbitTemplate;
      
          /**
           * SpringBoot2.6.x版本及该版本之后,使用`ReturnsCallback`接口
           */
          @PostConstruct
          public void init() {
              // 设置ReturnCallback
              rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
                  @Override
                  public void returnedMessage(ReturnedMessage returnedMessage) {
                      int replyCode = returnedMessage.getReplyCode();
                      String replyText = returnedMessage.getReplyText();
                      String exchange = returnedMessage.getExchange();
                      String routingKey = returnedMessage.getRoutingKey();
                      Message message = returnedMessage.getMessage();
                      log.error("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",replyCode, replyText, exchange, routingKey, message.toString());
                  }
              });
              /* Lambda表达式形式
              rabbitTemplate.setReturnsCallback(returnedMessage -> {
                  int replyCode = returnedMessage.getReplyCode();
                  String replyText = returnedMessage.getReplyText();
                  String exchange = returnedMessage.getExchange();
                  String routingKey = returnedMessage.getRoutingKey();
                  Message message = returnedMessage.getMessage();
                  log.error("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",replyCode, replyText, exchange, routingKey, message.toString());
              });*/
          }
      }

配置回调函数ConfirmCallback

目标:配置publisher-confirm的的回调机制所需的回调函数

注意:

  • 由于每个消息发送时的处理逻辑不一定相同,因此每个RabbitTemplate可以针对不同的业务处理方案配置不同的ConfirmCallback,此时属于局部配置。
  • 配置回调函数ConfirmCallback的示例在 配置回调函数ReturnCallback 的基础上演示。因此,以案例DemomessageConfirm/publisherConfirm/ReturnCallback-方式2/SpringBoot2.6.x之后作为基础框架
  • 原理:在SpringBoot新老版本的不同方式演示中,均调用了RabbitTemplate类中的convertAndSend()方法,源码如下:

    java 复制代码
    public void convertAndSend(String exchange, String routingKey, Object object, @Nullable CorrelationData correlationData) throws AmqpException {
            this.send(exchange, routingKey, this.convertMessageIfNecessary(object), correlationData);
        }

    convertAndSend()方法中有个参数CorrelationData,对应该类的源码如下

    • SpringBoot3.x版本之前:

      java 复制代码
      public class CorrelationData implements Correlation {
          private final SettableListenableFuture<Confirm> future = new SettableListenableFuture();
          private volatile String id;
          private volatile ReturnedMessage returnedMessage;
          ...
      }
    • SpringBoot3.x版本之后:

      java 复制代码
      public class CorrelationData implements Correlation {
          private final CompletableFuture<Confirm> future = new CompletableFuture();
          private volatile String id;
          private volatile ReturnedMessage returnedMessage;
          ...
      }
    • 从以上两个版本的CorrelationData类中可看出均有两个核心参数:

      1. id:消息的唯一标识,MQ对不同的消息的回执以此做判断,避免混淆
      2. 获取回执结果的Future对象:
        • 老版本为SettableListenableFuture
        • 新版本为CompletableFuture
    • 未来MQ的回执就会通过这个Future来返回,因此可以提前给CorrelationData中的Future接口的实现类添加回调函数来处理消息回执,所以在后续的代码实现步骤中,就需要去先获取到对应Future接口的实现类,然后在去配置回调函数

  • SpringBoot3.x版本之前方式及步骤如下:

    该版本案例Demo已上传至Gitee,可详见Gitee项目messageConfirm/publisherConfirm/ConfirmCallback/SpringBoot3.x之前

    • Step1: 在生产者模块(即publisher)的test包下创建测试类ConfirmCallbackTest,并在该类中设置消息体

    • Step2: 创建CorrelationData对象,并将消息ID封装到CorrelationData对象中(存在消息Id的情况下)

    • Step3: CorrelationData对象添加callback

      • SpringBoot3.x版本之前,CorrelationDatagetFuture()返回的是一个ListenableFuture<CorrelationData.Confirm>ListenableFutureaddCallback()方法,可以用来配置回调函数ConfirmCallback
      • 但是在SpringBoot3.x版本之后CorrelationDatagetFuture()返回的是一个CompletableFuture<CorrelationData.Confirm>CompletableFuture没有addCallback()方法,所以此时就不能用addCallback()来配置回调函数ConfirmCallback
    • Step4: 利用RabbitTemplate发送消息

      java 复制代码
      package at.guigu.mq;
      
      import lombok.RequiredArgsConstructor;
      import lombok.extern.slf4j.Slf4j;
      import org.junit.jupiter.api.Test;
      import org.springframework.amqp.rabbit.connection.CorrelationData;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import java.util.UUID;
      
      /**
       * 配置回调函数`ConfirmCallback`
       * SpringBoot3.x版本之前的方式
       * 注意:若使用的版本是SpringBoot3.x版本之后,则该代码会报错
       */
      @Slf4j
      @RequiredArgsConstructor
      public class ConfirmCallbackTest {
          private RabbitTemplate rabbitTemplate;
      
          @Test
          public void publisherConfirm() {
              // 设置消息体
              String message = "Hello ConfirmCallback!";
              // 将消息体对应的消息Id封装到CorrelationData中
              CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
              //添加callback
              correlationData.getFuture().addCallback(
                      result -> {
                          if (result.isAck()) {
                              log.debug("消息发送成功, ID:{}", correlationData.getId());
                          } else {
                              log.error("消息发送失败, ID:{}, 原因:{}", correlationData.getId(), result.getReason());
                          }
                      },
                      ex -> log.error("消息发送异常, ID:{}, 原因:{}", correlationData.getId(), ex.getMessage())
              );
              /* 等同于
              correlationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
                  @Override
                  public void onSuccess(CorrelationData.Confirm result) {
                      if (result.isAck()) {
                          log.debug("消息发送成功, ID:{}", correlationData.getId());
                      } else {
                          log.error("消息发送失败, ID:{}, 原因:{}", correlationData.getId(), result.getReason());
                      }
                  }
                  @Override
                  public void onFailure(Throwable ex) {
                      log.error("消息发送异常, ID:{}, 原因:{}", correlationData.getId(), ex.getMessage());
                  }
              });*/
              // 发送消息
              rabbitTemplate.convertAndSend("cgrs572.direct", "blue", message, correlationData);
          }
      }
  • SpringBoot3.x版本之后方式及步骤如下:

    在SpringBoot3.x版本之后CorrelationDatagetFuture()返回的是一个CompletableFuture<CorrelationData.Confirm>CompletableFuture没有addCallback()方法,所以此时就不能用addCallback()来配置回调函数ConfirmCallback。因此,此时可使用其它方式,博主只简单列举几种

    • 方式一:直接使用RabbitTemplatesetConfirmCallback()方法来设置

      该版本案例Demo已上传至Gitee,可详见Gitee项目messageConfirm/publisherConfirm/ConfirmCallback/SpringBoot3.x之后/setConfirmCallback-方式一
      注意: 创建的CorrelationData对象最好不要放在rabbitTemplate.setConfirmCallback之前,若要放在其之前,则必须保证创建的CorrelationData对象名与rabbitTemplate.setConfirmCallback的第一个参数名不一致(博主均命名为correlationData

      原因:虽然在运行时是同一个对象,但在编译时他们是两个独立的变量声明,报错是编译阶段的语法规则(与运行时的对象关系无关)

      java 复制代码
      package at.guigu.mq;
      
      import lombok.RequiredArgsConstructor;
      import lombok.extern.slf4j.Slf4j;
      import org.junit.jupiter.api.Test;
      import org.springframework.amqp.AmqpException;
      import org.springframework.amqp.rabbit.connection.CorrelationData;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      
      import java.util.Objects;
      import java.util.UUID;
      
      /**
       * SpringBoot3.x版本之后方式一
       * 直接使用`RabbitTemplate`的`setConfirmCallback()`方法配置回调函数`ConfirmCallback`
       */
      @Slf4j
      @SpringBootTest
      public class ConfirmCallbackTest {
      
          @Autowired
          private RabbitTemplate rabbitTemplate;
          @Test
          public void publisherConfirm() {
      
              rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
                  if (Objects.isNull(correlationData)) {
                      return;
                  }
                  if (ack) {
                      log.debug("消息发送成功, ID:{}", correlationData.getId());
                  } else {
                      log.error("消息发送失败, ID:{}, 原因:{}", correlationData.getId(), cause);
                  }
              });
      
              // 设置消息体
              String message = "Hello ConfirmCallback!";
              // 将消息体对应的消息Id封装到CorrelationData中
              CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
              // 发送消息
              try {
                  rabbitTemplate.convertAndSend("cgrs572.direct", "blue", message, correlationData);
              } catch (AmqpException ex) {
                  log.error("消息发送失败, ID:{}, 原因:{}", correlationData.getId(), ex.getMessage());
              }
          }
      }
    • 方式二:使用CompletableFuture中的whenComplete()方法

      该版本案例Demo已上传至Gitee,可详见Gitee项目messageConfirm/publisherConfirm/ConfirmCallback/SpringBoot3.x之后/whenComplete-方式二

      java 复制代码
      package at.guigu.mq;
      
      import lombok.RequiredArgsConstructor;
      import lombok.extern.slf4j.Slf4j;
      import org.junit.jupiter.api.Test;
      import org.springframework.amqp.rabbit.connection.CorrelationData;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      
      import java.util.Objects;
      import java.util.UUID;
      
      /**
       * SpringBoot3.x版本之后方式二
       * 直接使用`CompletableFuture`中的`whenComplete()`方法配置回调函数`ConfirmCallback`
       */
      @Slf4j
      @SpringBootTest
      public class ConfirmCallbackTest {
      
          @Autowired
          private RabbitTemplate rabbitTemplate;
      
          @Test
          public void publisherConfirm() {
              // 设置消息体
              String message = "Hello ConfirmCallback!";
              // 将消息体对应的消息Id封装到CorrelationData中
              CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
      
              // 使用`CompletableFuture`中的whenComplete()方法
              correlationData.getFuture().whenComplete((result, ex) -> {
                  if (Objects.nonNull(ex)) {
                      log.error("消息发送异常, ID:{}, 原因:{}", correlationData.getId(), ex.getMessage());
                  } else if (result.isAck()) {
                      log.debug("消息发送成功, ID:{}", correlationData.getId());
                  } else {
                      log.error("消息发送失败, ID:{}, 原因:{}", correlationData.getId(), result.getReason());
                  }
              });
      
              // 发送消息
              rabbitTemplate.convertAndSend("cgrs572.direct", "blue", message, correlationData);
      
          }
      }
    • 方式三:使用CompletableFuture中的thenAccept()方法

      该版本案例Demo已上传至Gitee,可详见Gitee项目messageConfirm/publisherConfirm/ConfirmCallback/SpringBoot3.x之后/thenAccept-方式三

      java 复制代码
      package at.guigu.mq;
      
      import lombok.extern.slf4j.Slf4j;
      import org.junit.jupiter.api.Test;
      import org.springframework.amqp.rabbit.connection.CorrelationData;
      import org.springframework.amqp.rabbit.core.RabbitTemplate;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      import java.util.UUID;
      
      /**
       * SpringBoot3.x版本之后方式三
       * 直接使用`CompletableFuture`中的`thenAccept()`方法配置回调函数`ConfirmCallback`
       */
      @Slf4j
      @SpringBootTest
      public class ConfirmCallbackTest {
          @Autowired
          private RabbitTemplate rabbitTemplate;
      
          @Test
          public void publisherConfirm() {
              // 设置消息体
              String message = "Hello ConfirmCallback!";
              // 将消息体对应的消息Id封装到CorrelationData中
              CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
      
              // 使用 thenAccept() 处理成功情况,exceptionally() 处理异常
              correlationData.getFuture()
                      .thenAccept(result -> {
                          if (result.isAck()) {
                              log.debug("消息发送成功, ID:{}", correlationData.getId());
                          } else {
                              log.error("消息发送失败, ID:{}, 原因:{}", correlationData.getId(), result.getReason());
                          }
                      })
                      .exceptionally(ex -> {
                          log.error("消息发送异常, ID:{}, 原因:{}", correlationData.getId(), ex.getMessage());
                          return null;
                      });
      
              // 发送消息
              rabbitTemplate.convertAndSend("cgrs572.direct", "blue", message, correlationData);
          }
      }
  • 注意:

    • 在SpringBoot新老版本的不同方式演示中,均调用了RabbitTemplate类中的convertAndSend()方法来发送消息

      java 复制代码
      public void convertAndSend(String exchange, String routingKey, Object object, @Nullable CorrelationData correlationData) throws AmqpException {
              this.send(exchange, routingKey, this.convertMessageIfNecessary(object), correlationData);
          }
    • convertAndSend()方法中有个参数CorrelationData,对应该类的源码如下

      java 复制代码
      public class CorrelationData implements Correlation {
          private final CompletableFuture<Confirm> future = new CompletableFuture();
          private volatile String id;
          private volatile ReturnedMessage returnedMessage;
      
          public CorrelationData() {
              this.id = UUID.randomUUID().toString();
          }
      
          public CorrelationData(String id) {
              Assert.notNull(id, "'id' cannot be null and must be unique");
              this.id = id;
          }
          ...
      }

测试类运行测试

  • 当交换机名称、RoutingKey都无任何问题时,运行测试类发送成功

测试ConfirmCallback

  • 生产者发送消息时将交换机名称替换为错误名称,运行测试类,结果如图所示,可知成功触发回调函数ReturnCallback,生产者发送失败

测试ReturnCallback

  • 生产者发送消息时将消息队列的名称替换为错误名称,运行测试类,结果如图所示,可知成功触发回调函数ConfirmCallback。消息成功投递到交换机,但交换机路由失败

数据持久化

由于RabbitMQ默认是内存存储消息,若RabbitMQ宕机或重启就会导致未持久化的交换机以及队列被删除,进而导致消息丢失。因此,为保证数据的持久化存储,就需要实现交换机、队列以及消息的持久化存储

交换机及队列持久化

注解形式

注意:

java 复制代码
package at.guigu.mq.listener;

import org.springframework.amqp.core.ExchangeTypes;
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;

/**
 * 消费者消费消息(包含队列创建、绑定及监听)
 */
@Component
public class SpringAmqpDirectExchangeListener {
	/** 
	* 参数durable:是否持久化,默认为true
	* @Queue的参数autoDelete:当最后一个消费者与当前队列断开连接时,是否自动删除队列
	* @Exchange的参数autoDelete:当没有队列绑定到当前交换机绑定时,是否自动删除交换机
	*/
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1", durable = "true", autoDelete = "false"), // 队列direct.queue1
            exchange = @Exchange(name = "cgrs572.direct", type = ExchangeTypes.DIRECT, durable = "true", autoDelete = "false"), // 交换机名称、类型、持久化
            key = {"red", "blue"} // BindingKey
    ))
    public void listenDirectQueue1(String msg) {
        System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
    }

    /** 
	* 参数durable:是否持久化,默认为true
	* @Queue的参数autoDelete:当最后一个消费者与当前队列断开连接时,是否自动删除队列
	* @Exchange的参数autoDelete:当没有队列绑定到当前交换机绑定时,是否自动删除交换机
	*/
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2", durable = "true", autoDelete = "false"), // 队列direct.queue2
            exchange = @Exchange(name = "cgrs572.direct", type = ExchangeTypes.DIRECT, durable = "true", autoDelete = "false"), // 交换机名称、类型、持久化
            key = {"red", "yellow"} // BindingKey
    ))
    public void listenDirectQueue2(String msg) {
        System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
    }
}

配置类形式

注意:

  • 修改消费者模块(即consumer模块)的config包下的配置类DirectConfig中声明交换机的代码

    java 复制代码
    package at.guigu.mq.config;
    
    import org.springframework.amqp.core.*;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class DirectConfig {
        /**
         * 声明DirectExchange交换机---cgrs572.direct
         * 参数name:指定Direct交换机名称
         * 参数durable:是否持久化,默认为true
         * 参数autoDelete:当没有队列绑定到当前交换机绑定时,是否自动删除交换机
         * @return 返回一个Direct Exchange交换机(路由机制)
         */
        @Bean
        public DirectExchange directExchange(){
            return new DirectExchange("cgrs572.direct", true, false);
        }
    
        /**
         * 声明消息队列1---direct.queue1
         * 参数name:指定要创建的消息队列名称
         * 参数durable:是否持久化,默认为true
         * @return 返回指定名称且持久化的消息队列
         */
        @Bean
        public Queue directQueue1(){
            return QueueBuilder.durable("direct.queue1").build();
            // 等同于
            // return new Queue("direct.queue1", true);
        }
    
        /**
         * 声明消息队列2---direct.queue2
         * 参数name:指定要创建的消息队列名称
         * 参数durable:是否持久化,默认为true
         * @return 返回指定名称且持久化的消息队列
         */
        @Bean
        public Queue directQueue2(){
            return QueueBuilder.durable("direct.queue2").build();
            // 等同于
            // return new Queue("direct.queue2", true);
        }
    
        // 批量绑定
        @Bean
        public Declarables bindings() {
            return new Declarables(
                    BindingBuilder.bind(directQueue1()).to(directExchange()).with("red"),
                    BindingBuilder.bind(directQueue1()).to(directExchange()).with("blue"),
                    BindingBuilder.bind(directQueue2()).to(directExchange()).with("red"),
                    BindingBuilder.bind(directQueue2()).to(directExchange()).with("yellow")
            );
        }
    }

消息持久化存储

注意:

  • 本示例是在 交换机及队列持久化示例 基础上进行演示
    • 由于消息持久化仅需在生产者发送消息时,对消息进行包装,不涉及消费者的内容。因此,此时示例仅选择注解形式一种来演示即可(原因:生产者发送消息的代码一致)
  • 本示例Demo可详见Gitee项目messagePersistence/messagePersistentStorage
  • 修改生产者模块(即publisher模块)的测试类中发送消息的代码如下

    java 复制代码
    package at.guigu.mq;
    
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    import org.springframework.amqp.AmqpException;
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.core.MessageBuilder;
    import org.springframework.amqp.core.MessageDeliveryMode;
    import org.springframework.amqp.rabbit.connection.CorrelationData;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import java.nio.charset.StandardCharsets;
    import java.util.Objects;
    import java.util.UUID;
    
    /**
     * SpringBoot3.x版本之后方式一
     * 直接使用`RabbitTemplate`的`setConfirmCallback()`方法配置回调函数`ConfirmCallback`
     */
    @Slf4j
    @SpringBootTest
    public class ConfirmCallbackTest {
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
        @Test
        public void publisherConfirm() {
    
            rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
                if (Objects.isNull(correlationData)) {
                    return;
                }
                if (ack) {
                    log.debug("消息发送成功, ID:{}", correlationData.getId());
                } else {
                    log.error("消息发送失败, ID:{}, 原因:{}", correlationData.getId(), cause);
                }
            });
    
            // 设置消息体
            String message = "Hello ConfirmCallback!";
            // 消息持久化
            Message msg = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))
                    .setDeliveryMode(MessageDeliveryMode.PERSISTENT) // 设置消息的投递方式持久化
                    .build();
            // 将消息体对应的消息Id封装到CorrelationData中
            CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
            // 发送消息
            try {
                rabbitTemplate.convertAndSend("cgrs572.direct", "blue", msg, correlationData);
            } catch (AmqpException ex) {
                log.error("消息发送失败, ID:{}, 原因:{}", correlationData.getId(), ex.getMessage());
            }
        }
    }

注意

  • 交换机、队列以及消息在默认情况下就已经是持久化,所以不需要做额外设置
  • 本小节说明数据持久化的方式是因为有些情况是不需要做持久化的,方便去关闭持久化

消费者消息确认

本示例是在 生产者消息确认示例 基础上进行演示

  • RabbitMQ支持消费者确认机制,即:消费者处理完消息后可以向MQ发送ACK回执,MQ收到确认消息后才会删除对应消息
  • SpringAMQP提供了三种消费者消息确认模式
    • none:关闭ACK,此时MQ假定消费者获取消息后会成功处理,因此在消息投递后不论是否被成功处理,都会被立即删除
      • 当消费者消息确认模式设置为none,此时若消费者消费失败,则会导致消息丢失
    • manual:手动ACK,需要在业务代码结束后,调用API发送ACK
    • auto(默认):自动ACK,由Spring检测listener代码(即消费者的监听器类)是否出现异常,若无异常则返回ACK;反之,则抛出异常并返回NACK
      • 底层使用的是AOP。由Spring自动完成
      • 当消费者消息确认模式设置为auto时,此时若消息处理失败,则消息会重新进入队列并再次发送给消费者,消费者则会重新消费该消息,以此循环往复,直至消息被消费成功
        • 因此,该模式下可能会进入死循环,带来不必要的压力。为解决该问题,可设置消费者的失败重试机制

示例:消息消费失败后被被立即删除

本示例Demo可详见Gitee项目messageConfirm/consumerConfirm/MessageProcessingFailed/drop

  • Step1: 在消费者模块(即consumer模块)的application.yml配置文件中添加相关代码即可配置消费者消息确认模式。

    yaml 复制代码
    spring:
      rabbitmq:
        listener:
          simple:
            prefetch: 1 # 每次只能获取一条消息,处理完成并触发了ACK确认机制后,才能获取下一个消息
            acknowledge-mode: none # none:关闭ACK    manual:手动ACK   auto:自动ACK
    • 配置文件完整代码如下

      yaml 复制代码
      logging:
        pattern:
          dateformat: MM-dd HH:mm:ss:SSS
      spring:
        rabbitmq:
          host: 192.168.204.128 # rabbitMQ的ip地址 
          port: 5672 # 配置RabbitMQ的消息发送处理接口(即消息通信接口)
          username: root # 配置RabbitMQ的用户名
          password: 123456 # 配置RabbitMQ的密码
          virtual-host: / # 配置可使用的虚拟主机
          listener:
            simple:
              prefetch: 1 # 每次只能获取一条消息,处理完成并触发了ACK确认机制后,才能获取下一个消息
              acknowledge-mode: none # none:关闭ACK    manual:手动ACK   auto:自动ACK(默认)
  • Step2: 修改消费者模块(即consumer模块)的监听器类SpringAmqpDirectExchangeListener中的消费消息的代码来模拟消息处理失败,代码修改如下

    java 复制代码
    package at.guigu.mq.listener;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.stereotype.Component;
    
    /**
     * 消费者监听并消费消息
     */
    @Slf4j
    @Component
    public class SpringAmqpDirectExchangeListener {
        @RabbitListener(queues = "direct.queue1")
        public void listenDirectQueue1(String msg) {
            log.info("消费者1接收到direct.queue1的消息:【" + msg + "】");
            System.out.println(1 / 0); // 模拟消费失败
            log.info("消费者1成功处理direct.queue1的消息");
        }
    
        @RabbitListener(queues = "direct.queue2")
        public void listenDirectQueue2(String msg) {
            log.info("消费者2接收到direct.queue2的消息:【" + msg + "】");
            System.out.println(1 / 0); // 模拟消费失败
            log.info("消费者2成功处理direct.queue2的消息");
        }
    }
  • Step3: 运行生产者模块(即publisher模块)发送消息的测试类,运行成功后可看到RabbitMQ客户端对应Queue成功接收到消息

  • Step4: 运行消费者模块(即consumer模块)的启动类,此时消费者在接收到消息消费时,由于System.out.println(1 / 0);会导致消费失败,进而导致消息被立即删除

    • 消费者消费失败截图如下

    • 此时RabbitMQ客户端对应Queue已将消息丢失

失败重试机制

本示例Demo可详见Gitee项目messageConfirm/consumerConfirm/MessageProcessingFailed/RetryOnFailure

  • 当配置消费者消息确认模式为auto时 ,若消息一直处理失败或抛出异常,则消费者会不断尝试消费该消息。为了避免死循环,我们可以利用Spring的retry机制,即在消费者处理失败或抛出异常时利用本地重试,而不是让消息无限制的重新回到消息队列
    • 注意:利用本地重试的方式就是配置失败重试机制

前置步骤

  • 在消费者模块(即consumer模块)的application.yml配置文件中配置消费者消息确认模式为auto,并设置失败重试机制

    yaml 复制代码
    spring:
      rabbitmq:
        listener:
          simple:
            prefetch: 1 # 每次只能获取一条消息,处理完成并触发了ACK确认机制后,才能获取下一个消息
            acknowledge-mode: auto # none:关闭ACK    manual:手动ACK   auto:自动ACK
            retry:
              enabled: true # 开启消费者失败重试机制(默认为false)
              initial-interval: 1000 # 初始的失败重试的间隔时间(ms)---消费者第一次消费失败后需要等待1s,然后再去重试
              multiplier: 2 # 失败重试间隔时间的倍数  下次等待时长 = initial-interval * multiplier^(n-1)  第一次等待1s,第二次等待2s,第三次等待4s
              max-attempts: 2 # 最大重试次数
              # 状态是否缺失:默认true缺失(即无状态),则每次重试都会创建一个新的consumer实例,不会保留之前的消费者状态和上下文信息,适合不需要维护消费状态的业务场景
              # false不缺失(即有状态),每次重试都是同一个consumer实例,适合需要维护消费状态的场景。若业务中包含事务,则必须为false,目的是为了在重试时避免事务失效
              stateless: true
    • 完整代码如下

      yaml 复制代码
      logging:
        pattern:
          dateformat: MM-dd HH:mm:ss:SSS
      spring:
        rabbitmq:
          host: 192.168.204.128 # rabbitMQ的ip地址
          port: 5672 # 配置RabbitMQ的消息发送处理接口(即消息通信接口)
          username: root # 配置RabbitMQ的用户名
          password: 123456 # 配置RabbitMQ的密码
          virtual-host: / # 配置可使用的虚拟主机
          listener:
            simple:
              prefetch: 1 # 每次只能获取一条消息,处理完成并触发了ACK确认机制后,才能获取下一个消息
              acknowledge-mode: auto # none:关闭ACK    manual:手动ACK   auto:自动ACK(默认)
              retry:
                enabled: true # 开启消费者失败重试机制(默认为false)
                initial-interval: 1000 # 初始的失败重试的间隔时间(ms)---消费者第一次消费失败后需要等待1s,然后再去重试
                multiplier: 2 # 失败重试间隔时间的倍数  下次等待时长 = initial-interval * multiplier^(n-1)  第一次等待1s,第二次等待2s,第三次等待4s
                max-attempts: 3 # 最大重试次数
                # 状态是否缺失:默认true缺失(即无状态),则每次重试都会创建一个新的consumer实例,不会保留之前的消费者状态和上下文信息,适合不需要维护消费状态的业务场景
                # false不缺失(即有状态),每次重试都是同一个consumer实例,适合需要维护消费状态的场景。若业务中包含事务,则必须为false,目的是为了在重试时避免事务失效
                stateless: true
    • stateless:状态是否缺失

      • 默认true缺失(即无状态),则每次重试都会创建一个新的consumer实例,不会保留之前的消费者状态和上下文信息,适合不需要维护消费状态的业务场景
      • false不缺失(即有状态),每次重试都是同一个consumer实例,适合需要维护消费状态的场景。若业务中包含事务,则必须为false,目的是为了在重试时避免事务失效
  • 注意:配置失败重试机制后,若达到最大重试次数时消息仍被消费失败,则消息就会被丢弃

消费者模块配置类形式步骤

注意:注解形式此处省略演示,因为即使是在注解形式中,也是给SpringAmqpDirectExchangeListener中添加相关错误代码来模拟消息处理失败的情况

  • 修改消费者模块(即consumer模块)的监听器类SpringAmqpDirectExchangeListener消费消息的代码来模拟消息处理失败,代码修改如下

    • 注意:利用log.info输出日志来输出消费者接收到消息,这样更能清除的验证所配置的失败重试机制
    java 复制代码
    package at.guigu.mq.listener;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.stereotype.Component;
    
    /**
     * 消费者监听并消费消息
     */
    @Slf4j
    @Component
    public class SpringAmqpDirectExchangeListener {
        @RabbitListener(queues = "direct.queue1")
        public void listenDirectQueue1(String msg) {
            log.info("消费者1接收到direct.queue1的消息:【" + msg + "】");
            System.out.println(1 / 0); // 模拟消费失败
            log.info("消费者1成功处理direct.queue1的消息");
        }
    
        @RabbitListener(queues = "direct.queue2")
        public void listenDirectQueue2(String msg) {
            log.info("消费者2接收到direct.queue2的消息:【" + msg + "】");
            System.out.println(1 / 0); // 模拟消费失败
            log.info("消费者2成功处理direct.queue2的消息");
        }
    }

运行演示

  • Step1: 运行生产者模块(即publisher模块)发送消息的测试类,运行成功后可看到RabbitMQ客户端对应Queue成功接收到消息

  • Step2: 运行消费者模块(即consumer模块)的启动类,此时消费者在接收到消息消费时,由于System.out.println(1 / 0);会导致消费失败,此时由于消费者消息确认模式为auto,且配置了失败重试机制,所以会进行重试

    • 消费者消费失败截图如下,由截图可知重试时间间隔会随着重试次数的增加而增加,并且在达到最大重试次数仍消费失败时,消息就会被丢弃

      • 注意:第一次消费消费时,若消费失败则本身就属于一次重试
    • 达到最大重试次数仍消费失败,RabbitMQ客户端对应Queue就会将消息丢失

消息重试失败后的处理策略

本示例Demo可详见Gitee项目messageConfirm/consumerConfirm/MessageProcessingFailed/RetryFailureHandlingStrategy

  • 在配置失败重试机制后,若达到最大重试次数时消息仍被消费失败,此时就会由MessageRecover接口来对消息进行处理,有三个对应实现类对消息进行处理

    • RejectAndDontRequeueRecoverer(失败重试机制默认):达到最大重试次数若消息仍被消费失败,则直接reject,丢弃消息

    • ImmediateRequeueMessageRecover:达到最大重试次数若消息仍被消费失败,则直接返回NACK,消息会重新入队

    • RepublishMessageRecoverer(推荐):达到最大重试次数若消息仍被消费失败,则会将消息投递到指定的交换机

前置公共步骤

  • 在消费者模块(即consumer模块)的application.yml配置文件中配置消费者消息确认模式为auto,并设置失败重试机制

    yaml 复制代码
    spring:
      rabbitmq:
        listener:
          simple:
            prefetch: 1 # 每次只能获取一条消息,处理完成并触发了ACK确认机制后,才能获取下一个消息
            acknowledge-mode: auto # none:关闭ACK    manual:手动ACK   auto:自动ACK
            retry:
              enabled: true # 开启消费者失败重试机制(默认为false)
              initial-interval: 1000 # 初始的失败重试的间隔时间(ms)---消费者第一次消费失败后需要等待1s,然后再去重试
              multiplier: 2 # 失败重试间隔时间的倍数  下次等待时长 = initial-interval * multiplier^(n-1)  第一次等待1s,第二次等待2s,第三次等待4s
              max-attempts: 2 # 最大重试次数
              # 状态是否缺失:默认true缺失(即无状态),则每次重试都会创建一个新的consumer实例,不会保留之前的消费者状态和上下文信息,适合不需要维护消费状态的业务场景
              # false不缺失(即有状态),每次重试都是同一个consumer实例,适合需要维护消费状态的场景。若业务中包含事务,则必须为false,目的是为了在重试时避免事务失效
              stateless: true
    • 完整代码示例如下

      yaml 复制代码
      logging:
        pattern:
          dateformat: MM-dd HH:mm:ss:SSS
      spring:
        rabbitmq:
          host: 192.168.204.128 # rabbitMQ的ip地址
          port: 5672 # 配置RabbitMQ的消息发送处理接口(即消息通信接口)
          username: root # 配置RabbitMQ的用户名
          password: 123456 # 配置RabbitMQ的密码
          virtual-host: / # 配置可使用的虚拟主机
          listener:
            simple:
              prefetch: 1 # 每次只能获取一条消息,处理完成并触发了ACK确认机制后,才能获取下一个消息
              acknowledge-mode: auto # none:关闭ACK    manual:手动ACK   auto:自动ACK
              retry:
                enabled: true # 开启消费者失败重试机制(默认为false)
                initial-interval: 1000 # 初始的失败重试的间隔时间(ms)---消费者第一次消费失败后需要等待1s,然后再去重试
                multiplier: 2 # 失败重试间隔时间的倍数  下次等待时长 = initial-interval * multiplier^(n-1)  第一次等待1s,第二次等待2s,第三次等待4s
                max-attempts: 3 # 最大重试次数
                # 状态是否缺失:默认true缺失(即无状态),则每次重试都会创建一个新的consumer实例,不会保留之前的消费者状态和上下文信息,适合不需要维护消费状态的业务场景
                # false不缺失(即有状态),每次重试都是同一个consumer实例,适合需要维护消费状态的场景。若业务中包含事务,则必须为false,目的是为了在重试时避免事务失效
                stateless: true

消费者模块配置类形式步骤

  • Step1: 在消费者模块(即consumer模块)的config包下创建配置类ErrorMsgConfig

    • Step1-1: 在该类中声明接收失败消息的交换机、队列、两者之间绑定关系的Bean及所用到的BindingKey

    • Step1-2: 在该类中将消息重试失败后的处理策略改为RepublishMessageRecoverer

      • 注意:RepublishMessageRecoverer对应Bean的声明也可放在启动类中,此处不再演示
    java 复制代码
    package at.guigu.mq.config;
    
    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.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.amqp.rabbit.retry.MessageRecoverer;
    import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class ErrorMsgConfig {
        /**
         * 声明接收失败消息的DirectExchange交换机---cgrs572error.direct
         * 参数name:指定Direct交换机名称
         * @return 返回一个Direct Exchange交换机(路由机制)
         */
        @Bean
        public DirectExchange errorMessageExchange() {
            return new DirectExchange("cgrs572error.direct");
        }
    
        /**
         * 声明接收失败消息的消息队列---errordirect.queue
         * 参数name:指定要创建的消息队列名称
         * @return 返回指定名称且持久化的消息队列
         */
        @Bean
        public Queue errorQueue() {
            return new Queue("errordirect.queue");
        }
    
        /**
         * 绑定交换机与队列
         */
        @Bean
        public Binding errorBinding() {
            return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error");
        }
    
        @Bean
        public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate) {
            return new RepublishMessageRecoverer(rabbitTemplate, "cgrs572error.direct", "error");
        }
    }

后置公共步骤

  • 修改消费者模块(即consumer模块)的监听器类SpringAmqpDirectExchangeListener消费消息的代码来模拟消息处理失败,代码修改如下

    • 注意:利用log.info输出日志来输出消费者接收到消息,这样更能清除的验证所配置的失败重试机制
    java 复制代码
    package at.guigu.mq.listener;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.stereotype.Component;
    
    /**
     * 消费者监听并消费消息
     */
    @Slf4j
    @Component
    public class SpringAmqpDirectExchangeListener {
        @RabbitListener(queues = "direct.queue1")
        public void listenDirectQueue1(String msg) {
            log.info("消费者1接收到direct.queue1的消息:【" + msg + "】");
            System.out.println(1 / 0); // 模拟消费失败
            log.info("消费者1成功处理direct.queue1的消息");
        }
    
        @RabbitListener(queues = "direct.queue2")
        public void listenDirectQueue2(String msg) {
            log.info("消费者2接收到direct.queue2的消息:【" + msg + "】");
            System.out.println(1 / 0); // 模拟消费失败
            log.info("消费者2成功处理direct.queue2的消息");
        }
    }

运行演示

  • Step1: 运行消费者模块(即consumer模块)的启动类

    • 在RabbitMQ客户端可看到对应交换机、队列均成功创建并绑定

  • Step2: 运行生产者模块(即publisher模块)发送消息的测试类,此时消费者控制台如下图所示,显示达到最大重试次数仍消费失败,则将消息投递到指定接收失败消息的交换机

    • 再次进入RabbitMQ控制台可看到,接收消费失败消息的交换机已将消息发送到与其绑定的队列errordirect.queue中,如图一所示。进入该队列可发现此时接收的不仅有消息,还包括消费失败时的栈信息,如图二所示

相关推荐
卷心菜的学习路1 小时前
基于 Spring Cloud Tencent 元数据路由实现个人开发泳道,告别频繁部署
java·spring cloud
水无痕simon2 小时前
3 RabbitMQ基础
分布式·rabbitmq·ruby
xieliyu.2 小时前
UPD协议结构以及开发中注意事项
java·开发语言·笔记·java-ee
张宇Joaquin2 小时前
openEuler编译和更换内核的方法
java·linux·服务器
程序员黑豆2 小时前
Java正则表达式详解
java·前端·ai编程
Flynt3 小时前
我试了Spring Boot 4的原生镜像,聊聊那些数字和坑
java·spring boot
凤山老林3 小时前
复杂检索引擎落地:Spring Boot + Elasticsearch 数据同步与高阶查询实战
spring boot·后端·elasticsearch
M-Robots echo10 小时前
轻量化裁剪方案解析:M-Robots 模块化构建,适配高低端各类机器人硬件平台
java·机器人·ros·开源社区·atomgit·m-robots
不可求~10 小时前
多模型路由怎么落地:把任务分流写进项目的最小实现
java·前端·数据库