RabbitMQ的死信队列和延迟队列
死信队列
死信的概念
死信,就是无法被消费的消息,一般来说,producer将消息投递到broker或者直接到了queue里了,consumer从queue取消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。
死信产生的情况:
- 消息 TTL 过期
- 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
- 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false
普通队列上配置死信交换机
死信队列是在普通队列的基础上的扩展,有死信队列就一定有死信交换机,只需要将普通队列和死信交换机进行“关联”,将处理不了的消息发送至死信交换机,再由死信交换机发送到与其绑定的队列上,就完成了死信队列的使用。重点部分:为普通队列配置死信交换机
普通队列上配置死信交换机的关键在于,队列声明时对arguments参数的配置。
arguments参数是一个Map集合。
public class Consumer {
// 普通交换机
private static final String NONE_EXCHANGE = "none_exchange";
// 死信交换机
private static final String DEAD_EXCHANGE = "dead_exchange";
// 普通队列
private static final String NONE_QUEUE = "none_queue";
// 死信队列
private static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtils.getChannel();
// 声明普通交换机和死信交换机,【正常的声明】
channel.exchangeDeclare(NONE_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
// 声明死信队列,【正常的声明】
channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
// 死信队列与死信交换机绑定,【正常的绑定】
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");
/**
* 上面操作已经正常声明了死信交换机和死信队列并将它们绑定
*
* 在声明普通队列时需要配置其与死信交换机的绑定关系
*/
// 正常队列绑定死信队列信息
Map<String, Object> params = new HashMap<>();
//正常队列设置死信交换机 参数 key 是固定值
params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//正常队列设置死信 routing-key 参数 key 是固定值
params.put("x-dead-letter-routing-key", "lisi");
// 声明普通队列,将构建的参数传入
channel.queueDeclare(NONE_QUEUE, false, false, false, params);
// 普通队列与普通交换机绑定关系
channel.queueBind(NONE_QUEUE, NONE_EXCHANGE, "zhangsan");
System.out.println("等待接收消息...");
channel.basicConsume(NONE_QUEUE, false, new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
long deliveryTag = message.getEnvelope().getDeliveryTag();
if (deliveryTag == 5L) {
channel.basicReject(deliveryTag, false);
} else {
System.out.println(new String(message.getBody(), StandardCharsets.UTF_8));
channel.basicAck(deliveryTag, false);
}
}
}, new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
}
});
}
}
消息进入死信队列的三种情况详解
消息 TTL 过期
消息TTL过期是由发送者发送消息时指定消息的ttl时间。
需要注意的是,消息是否过期是在即将发送给消费者前判断的。因为存储消息的数据结构是队列的形式,所以如果排在前面的消息没有处理完,即使当前消息已经过期,并不能立即进入死信队列。
public class Producer {
private static final String NONE_EXCHANGE = "none_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMQUtils.getChannel();
// 设置消息的TTL时间,消息过期后进入死信队列,expiration参数的单位毫秒
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().expiration("5000").build();
for (int i = 0; i < 10; i++) {
String msg = "消息:" + (i + 1);
channel.basicPublish(NONE_EXCHANGE, "zhangsan", properties, msg.getBytes(StandardCharsets.UTF_8));
System.out.println("生产者发送消息:" + msg);
}
}
}
队列达到最大长度
此效果可以将队列的长度设置的小点,来观察。
// 设置正常队列长度的限制
params.put("x-max-length", 6);
此参数需要在声明队列时传入,注意此时需要把原先队列删除 因为参数改变了。
测试
生产者发送10条消息。
在控制台可以看到,普通队列最大只能容纳6条消息,还有4条消息会被放入死信队列中。
消息被拒
消费者受用channel.basicReject和channel.basicNack拒收消息。这两个方法都有一个requeue参数,代表被拒收的消息收费重新入队,如果为true,消息会再次进入队列,然后再发送过来;如果为false代表拒绝重新入队该队列如果配置了死信交换机将发送到死信队列中
延迟队列
延迟队列概念
延迟队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。换句话说,如果一条消息设置了 TTL 属性或者进入了设置 TTL 属性的队列,那么这条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的 TTL 和消息的
TTL,那么较小的那个值将会被使用,有两种方式设置 TTL
消息设置 TTL
针对每条消息设置ttl
队列设置 TTL
创建队列的时候设置队列的“x-message-ttl”属性
两者的区别
如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中),而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;另外,还需要注意的一点是,如果不设置 TTL,表示消息永远不会过期,如果将 TTL 设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃。
使用
就是在上述介绍的死信队列的基础上,为普通队列设置ttl过期时间