如果此教程对你有帮助,有钱的捧个钱场,没钱的请捧个人场!
1、背景说明:
开发环境:sprint boot,rabbitmq,mysql,jdk1.8...
在预生产环境中,消费者消费消息时出现了异常,后台不断写日志,一天时间日志文件超过了30G,导致服务器因磁盘空间不足而宕机!
2、原因分析:
1)rabbitmq的默认配置spring.rabbitmq.listener.default-requeue-rejected=true,消息消费失败,则重新放回队列,再次消费时依然失败,又被重新放回队列,从而陷入死循环......后台不停地写日志;
2)消费者消费消息的程序中没有用try_catch_来捕获可能出现的异常;
3、解决方案:
方案一:统一配置
## 消息消费失败,则丢弃消息
spring.rabbitmq.listener.default-requeue-rejected=false
方案二:添加消息消费失败监听事件
ListenerContainerConsumerFailedEvent
方案三:死信队列
Dead-Letter-Exchange
方案四:手动确认
autoAck = true
本文主要探讨死信队列的解决方案!
4、知识点简介
DLX, Dead-Letter-Exchange。利用DLX, 当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX。消息变成死信一般有一下几种情况:
1)消息被拒绝(basic.reject/ basic.nack)并且requeue=false;
2)消费消息时程序出现了异常;
3)消息过期(x-message-ttl);
4)队列中有消息数量超过了最大值(x-max-length);
5)队列中的消息容量超过了队列的最大空间(x-max-length-bytes);
其他情况,作者尚未探索,欢迎读者留言!
DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性,当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列,可以监听这个队列中消息做相应的处理,这个特性可以弥补RabbitMQ 3.0以前支持的immediate参数(可以参考RabbitMQ之mandatory和immediate)的功能。
5、案例代码
编码步骤:
1)创建普通交换机;
2)创建普通队列,设置队列属性x-dead-letter-exchange,与死信交换机绑定;设置队列其他属性...;
3)绑定普通队列到普通交换机上;
4)创建死信交换机;
5)创建死信队列;
6)绑定死信队列到死信交换机上,并指定死信路由键;
生产者:
/** 生产者只需向交换机中发送消息 */
public class MessageProducer {
/* 普通交换机 */
private static String exchangeName = "common_exchange";
private static void noExpirationDemo_2() {
try {
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, false);
for (int i = 0; i < 10; i++) {
String msg = "hello rabbitmq " + i;
channel.basicPublish(exchangeName, "", null, msg.getBytes());
}
System.out.println("消息发布成功");
channel.close();
connection.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
普通消息者:
/* 普通消息者 */
public class CommonConsumer {
/* 普通队列 */
private static String queueName = "common_queue";
/* 普通交换机 */
private static String exchangeName = "common_exchange";
/* 死信交换机 */
private static String dlxExchangeName = "dlx_exchange";
public static void main(String[] args) {
try {
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
Map<String, Object> arguments = new HashMap<>();
/* 当队列消息长度大于最大长度、或者过期的等,将从队列中删除的消息推送到指定的交换机中去而不是丢弃掉 */
arguments.put("x-dead-letter-exchange", dlxExchangeName);
/* 统一为整个队列的所有消息设置生命周期,注意时间值类型为int,单位为毫秒; */
// arguments.put("x-message-ttl", 5000);
/* 限定队列的消息数量的最大值,超过指定长度将会把最早的几条删除掉 */
// arguments.put("x-max-length", 5);
/* 限定队列最大占用的空间大小 */
arguments.put("x-max-length-bytes", 5);
channel.queueDeclare(queueName, false, false, false, arguments);
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, false);
channel.queueBind(queueName, exchangeName, "");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
System.out.println("common consumer receive :" + new String(body));
}
};
channel.basicConsume(queueName, true,consumer);
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
死信消费者:
/* 创建列信队列消费者 */
public class DlxConsumer {
/* 死信队列 */
private static String dlxQueueName = "dlx_queue";
/* 死信交换机 */
private static String dlxExchangeName = "dlx_exchange";
/* 死信路由键 */
private static String dlxRoutingKey = "";
public static void main(String[] args) {
try {
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(dlxQueueName, false, false, false, null);
channel.exchangeDeclare(dlxExchangeName, BuiltinExchangeType.FANOUT, false);
channel.queueBind(dlxQueueName, dlxExchangeName, dlxRoutingKey);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
System.out.println("dlx consumer receive :" + new String(body));
}
};
channel.basicConsume(dlxQueueName,true, consumer);
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
6、结果展示:
7、在springboot demo中的代码:
/* 配置死信队列 */
/* 死信交换机 */
private static String dlxExchangeName = "dlx_exchange";
/* 死信队列 */
private static String dlxQueueName = "dlx_queue";
/* 死信路由键 */
private static String dlxRoutingKey = "routingkey.dead";
/* 1、声明死信交换机 */
@Bean(name="dlxExchange")
public DirectExchange dlxExchange() {
return new DirectExchange(dlxExchangeName);
}
/* 2、声明死信队列 */
@Bean(name="dlxQueue")
public Queue dlxQueue() {
return new Queue(dlxQueueName);
}
/* 3、绑定死信队列与死信交换机 */
@Bean
Binding bindingDlxExchangeQueue(@Qualifier("dlxQueue") Queue dlxQueue, @Qualifier("dlxExchange") DirectExchange dlxExchange) {
return BindingBuilder.bind(dlxQueue).to(dlxExchange).with(dlxRoutingKey);
}
/* 4、修改普通队列的配置,消息消费失败后将路由到死信队列中 */
@Bean
public Queue queue() {
Map<String, Object> arguments = new HashMap<>();
/* 当队列消息长度大于最大长度、或者过期的等,将从队列中删除的消息推送到指定的交换机中去而不是丢弃掉 */
arguments.put("x-dead-letter-exchange", dlxExchangeName);
/* 将删除的消息推送到指定的交换机对应的路由键 */
arguments.put("x-dead-letter-routing-key", dlxRoutingKey);
Queue queue_ = new Queue(queue,true,false,false,arguments);
return queue_;
}