记录一次RabbitMQ的使用及问题

594 阅读4分钟

记录一次RabbitMQ的使用及问题

场景

有两个MQ队列,一个业务队列,一个死信队列。两个交换机的模式都是直连模式。大概的思路就是,正常接口发送消息到业务队列,业务队列的监听者调用业务处理逻辑进行处理;如果出现异常,则设置确定消息为false,这样消息就会流转到死信队列中去,在死信监听器中会有一些补偿逻辑。简易的逻辑图如下:

image.png

实现

1. MQ的配置

@Configuration
public class MqConfig {

/**
  * 交换机
  */
  
// 业务交换机
public static final String E_BUSINESS = "exchange_business";

//死信交换机
public static final String E_DEAD_LETTER = "exchange_dead_letter";


/*
 * 消息队列(queue),
 */

// 业务队列
public static final String Q_BUSINESS = "queue_business";

// 死信队列
public static final String Q_DEAD_LETTER = "queue_dead_letter";

/*
 * 路由关键字(routing_key)
 */
 
// 业务执行路由关键字
public static final String R_BUSINESS_EXECUTE = "routing_key_business_execute";

// 死信关键字
public static final String R_DEAD_LETTER = "routing_key_dead_letter";


/**
  * 业务交换机
  *
  * @return 业务交换机
  */
  @Bean
  public DirectExchange exchangeBusiness(){
      return new DirectExchange(E_BUSINESS, true, false);
  }

/**
  * 死信交换机
  *
  * @return 死信交换机
  */
  @Bean
  public DirectExchange exchangeDeadLetter(){
      return new DirectExchange(E_DEAD_LETTER, true, false);
  }

/**
  * 业务执行队列
  *
  * @return 业务执行队列
  */
  @Bean
  public Queue queueBusiness() {
     Map<String, Object> args = new HashMap<>(4);
     // 消息未收到确定消息时转发的交换机
     args.put("x-dead-letter-exchange", E_DEAD_LETTER);
     // 消息未收到确定消息时转发的路由关键字
     args.put("x-dead-letter-routing-key", R_DEAD_LETTER);

     return new Queue(Q_BUSINESS, true, false, false, args);
  }

/**
  * 死信队列
  *
  * @return 死信队列
  */
  @Bean
  public Queue queueDeadLetter() {
     return new Queue(Q_DEAD_LETTER, true, false, false, null);
    }

/**
  * 把[业务路由关键字]绑定到业务消息处理队列和业务交换机
  *
  * @return 绑定关系
  */
  @Bean
  public Binding bindingRoutingkeyBusiness(){
      return BindingBuilder.bind(queueBusiness()).to(exchangeBusiness()).with(R_BUSINESS_EXECUTE);
  }

/**
  * 把[死信路由关键字]绑定到死信消息处理队列和死信交换机
  *
  * @return 绑定关系
  */
   @Bean
   public Binding bindingRoutingkeyDeadLetter(){
       return BindingBuilder.bind(queueDeadLetter()).to(exchangeDeadLetter()).with(R_DEAD_LETTER);
  }
}

2. 业务消费者

@Component
public class BusinessExecuteLister {
    @RabbitHandler
    @RabbitListener(bindings = {
            @QueueBinding(value = @Queue(value = MqConfig.Q_BUSINESS), exchange = @Exchange(value = MqConfig.E_BUSINESS), key = MqConfig.R_BUSINESS_EXECUTE)})
    public void businessExecuteHandle(Channel channel, Message message, String content) {
        // 执行业务逻辑
    }
}

3. 死信消费者

@Component
public class DeadLetterLister {

    @RabbitHandler
    @RabbitListener(bindings = {
            @QueueBinding(value = @Queue(value = MqConfig.Q_DEAD_LETTER), exchange = @Exchange(value = MqConfig.E_DEAD_LETTER), key = MqConfig.R_DEAD_LETTER)})
    public void deadLetterHandle(Channel channel, Message message, String content) {
        // 执行补偿逻辑
    }
}

问题

首先上面的代码运行的时候肯定是会报错的,这篇文章的主题也不是这个逻辑的实现,而主要是记录一下碰到的问题。

错误截图&代码

image.png

INFO  [main] org.springframework.amqp.rabbit.core.RabbitAdmin - Auto-declaring a non-durable, auto-delete, or exclusive Queue (spring.gen-FE1e0qBaSBmSxXT-Q9LhvQ) durable:false, auto-delete:true, exclusive:true. It will be redeclared if the broker stops and is restarted while the connection factory is alive, but all messages will be lost.
ERROR [AMQP Connection] o.s.a.rabbit.connection.CachingConnectionFactory - Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'x-dead-letter-exchange' for queue 'queue_business' in vhost 'dev': received none but current is the value 'exchange_dead_letter' of type 'longstr', class-id=50, method-id=10)

问题1:业务队列和业务路由关键字未绑定

image.png

  • 问题描述: 程序启动时候,创建RabbitMq队列出现错误信息(这个在下面说),然后去RabbitMq的控制台查看的时候,发现交换机和队列创建成功,但是队列和路由关键字没有绑定。其实这个问题也是因为RabbitMq创建队列的时候出错的引起。

  • 问题原因: 其实解决方法只要正确的创建队列即可,这里主要是探究一下发生错误的原因。在这个案例中Mq队列是Spring容器帮我们创建的,因此我们可以来看一下Spring自动创建Mq队列的步骤,就能够找到没有绑定关系的原因了。

    1. 首先我们找到RabbitAutoConfiguration,RabbitMq的自动配置类。 image.png
    2. 然后找到RabbitTemplateConfiguration静态类,里面注册了一个RabbitAdmin的Bean image.png
    3. 进入到这个RabbitAdmin类,可以看到它实现了InitializingBean接口 image.png
    4. 然后在RabbitAdmin方法中实现了InitializingBean接口的afterPropertiesSet()方法,然后这个方法里面有一个核心的initialize()初始化方法。这个方法里分为三部分。 image.png image.png image.png
    5. 然后在初始化队列,也就是执行这句代码的时候 declareQueues(channel, queues.toArray(new Queue[queues.size()]));报错了,所以后面的队列和路由关键字的绑定就没有绑定上去,导致我们在MQ控制中看到的Mq队列是没有绑定路由关键字的

问题2:控制台报错,队列无法正常使用

  • 问题描述: 见上面的错误代码和截图,可以创建队列和交换机,但是无法绑定关键字,导致无法使用。

  • 问题原因: 创建队列的时候发现两个同名的队列属性不一致,导致报错。我们可以查看一下报错信息,然后去排查一下原因。

    1. 报错信息是reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'x-dead-letter-exchange' for queue 'queue_business' in vhost 'dev': received none but current is the value 'exchange_dead_letter' of type 'longstr', class-id=50, method-id=10)。这里可以看出来是队列冲突,前面是错误代码和错误文案,后面提示queue_business队列属性不相等,接收到的是none,但是当前是exchange_dead_letter。
    2. 然后我们可以查阅一下RabbitMq官方文档。 image.png
    3. 然后我们在队列基础中可以看到,队列声明的介绍。报错原因就藏在这里了。 image.png
  • 解决办法: 这里解决方法有两种

    1. 消费者和队列声明使用相同的属性,例如这里的案例,就可以在消费者加上相关的属性。
      @RabbitHandler
      @RabbitListener(bindings = {
      @QueueBinding(value = @Queue(value = MqConfigTest.Q_BUSINESS,
                                   arguments = {@Argument(name = "x-dead-letter-exchange", value = MqConfigTest.E_DEAD_LETTER),
                                                @Argument(name = "x-dead-letter-routing-key", value = MqConfigTest.R_DEAD_LETTER)}),
              exchange = @Exchange(value = MqConfigTest.E_BUSINESS),
              key = MqConfigTest.R_BUSINESS_EXECUTE)})
      public void businessExecuteHandle(Channel channel, Message message, String content) {
      // 执行业务逻辑
      }   
      
    2. 交换机使用广播模式。
    @Bean
    public FanoutExchange exchangeBusiness(){
    return new FanoutExchange(MqConfigTest.E_BUSINESS, true, false);
    }
    

解决效果

这里我使用的是第一种方式。最后看到队列和路由关键字正常绑定,并且控制台没有报错。队列也可以正常使用。

image.png

参考文章