记录一次RabbitMQ的使用及问题
场景
有两个MQ队列,一个业务队列,一个死信队列。两个交换机的模式都是直连模式。大概的思路就是,正常接口发送消息到业务队列,业务队列的监听者调用业务处理逻辑进行处理;如果出现异常,则设置确定消息为false,这样消息就会流转到死信队列中去,在死信监听器中会有一些补偿逻辑。简易的逻辑图如下:
实现
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) {
// 执行补偿逻辑
}
}
问题
首先上面的代码运行的时候肯定是会报错的,这篇文章的主题也不是这个逻辑的实现,而主要是记录一下碰到的问题。
错误截图&代码
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:业务队列和业务路由关键字未绑定
-
问题描述: 程序启动时候,创建RabbitMq队列出现错误信息(这个在下面说),然后去RabbitMq的控制台查看的时候,发现交换机和队列创建成功,但是队列和路由关键字没有绑定。其实这个问题也是因为RabbitMq创建队列的时候出错的引起。
-
问题原因: 其实解决方法只要正确的创建队列即可,这里主要是探究一下发生错误的原因。在这个案例中Mq队列是Spring容器帮我们创建的,因此我们可以来看一下Spring自动创建Mq队列的步骤,就能够找到没有绑定关系的原因了。
- 首先我们找到
RabbitAutoConfiguration,RabbitMq的自动配置类。 - 然后找到
RabbitTemplateConfiguration静态类,里面注册了一个RabbitAdmin的Bean - 进入到这个
RabbitAdmin类,可以看到它实现了InitializingBean接口 - 然后在
RabbitAdmin方法中实现了InitializingBean接口的afterPropertiesSet()方法,然后这个方法里面有一个核心的initialize()初始化方法。这个方法里分为三部分。 - 然后在初始化队列,也就是执行这句代码的时候
declareQueues(channel, queues.toArray(new Queue[queues.size()]));报错了,所以后面的队列和路由关键字的绑定就没有绑定上去,导致我们在MQ控制中看到的Mq队列是没有绑定路由关键字的
- 首先我们找到
问题2:控制台报错,队列无法正常使用
-
问题描述: 见上面的错误代码和截图,可以创建队列和交换机,但是无法绑定关键字,导致无法使用。
-
问题原因: 创建队列的时候发现两个同名的队列属性不一致,导致报错。我们可以查看一下报错信息,然后去排查一下原因。
- 报错信息是
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。 - 然后我们可以查阅一下RabbitMq官方文档。
- 然后我们在队列基础中可以看到,队列声明的介绍。报错原因就藏在这里了。
- 报错信息是
-
解决办法: 这里解决方法有两种
- 消费者和队列声明使用相同的属性,例如这里的案例,就可以在消费者加上相关的属性。
@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) { // 执行业务逻辑 } - 交换机使用广播模式。
@Bean public FanoutExchange exchangeBusiness(){ return new FanoutExchange(MqConfigTest.E_BUSINESS, true, false); } - 消费者和队列声明使用相同的属性,例如这里的案例,就可以在消费者加上相关的属性。
解决效果
这里我使用的是第一种方式。最后看到队列和路由关键字正常绑定,并且控制台没有报错。队列也可以正常使用。