6. 死信队列
6.1 死信的概念
死信,顾名思义就是无法被消费的消息。
字面意思可以这样理解,一般来说,producer(生产者)将消息投递到 broker 或者直接到 queue(队列)里了。consumer(消费者)从queue中取出消息后进行消费,但某些时候由于特定的原因导致 queue 中的消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到RabbitMQ的死信队列机制。当消息消费发生异常时,将消息投入死信队列中。比如:用户在商城下单,点击去支付后但在指定时间内未支付时自动失效。
6.2 死信的来源
- 消息 TTL(存活时间)过期
- 队列达到最长长度(队列满了,无法再添加数据)
- 消息被拒绝(basic.trject或basic.nack 消费者进行了否定应答或拒绝应答)并且 requeue=false(消息不放回队列中)
6.3 死信实战
6.3.1 代码架构图
- 生产者只通过普通交换机,经过普通信道将消息放入普通队列
- 普通队列在声明的时候会设置与死信队列相关联
- 普通队列中的消息如果存在 消息被拒绝、消息TTL过期、队列达到最大长度 中的一种情况,那么这些消息就会成为死信,就会通过普通队列与死信队列之间的信道,将消息传递给死信队列
- 死信队列也是普通的队列,只不过是存放了死信
6.3.2 消息 TTL 过期
在这里我们将模仿普通队列中消息TTL过期的情况,编写死信队列的代码,熟悉消息成为死信的流程。
上面的图说明,在消费者 C1 中需要声明 普通队列normal-queue、普通信道zhangsan 、普通交换机normal_exchange 以及 死信队列dead-queue、死信信道lisi、死信交换机dead_exchange。
消费者C1:
package com.example.eight;
import com.example.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
/**
* @author 且听风吟
* @version 1.0
* @description: 死信队列 实战
* 消费者 1
* @date 2022/4/23 0023 15:23
*/
public class Consumer01 {
//普通交换机 名称
public static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机 名称
public static final String DEAD_EXCHANGE = "dead_exchange";
//普通队列 名称
public static final String NORMAL_QUEUE = "normal_queue";
//死信队列 名称
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws IOException, TimeoutException {
//通过工具类获取信道
Channel channel = RabbitUtils.getChannel();
//声名死信和普通交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);
//声名普通队列
Map<String, Object> var5 = new HashMap<>();
//设置普通队列消息过期时间 10s=10000ms 最好由生产者指定
//var5.put("x-message-ttl",10000);
//给正常队列设置死信交换机
var5.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信队列的 routingKey
var5.put("x-dead-letter-routing-key","lisi");
channel.queueDeclare(NORMAL_QUEUE,false,false,false,var5);
//声名死信队列
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
//绑定普通交换机与普通队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");
//绑定死信交换机与死信队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
System.out.println("等待接收消息·····");
//消息消费成功之后的回调
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("Consumer01接受的消息为:"+new String(message.getBody(),"UTF-8"));
};
channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,consumer->{});
}
}
消费者C2:
package com.example.eight;
import com.example.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
/**
* @author 且听风吟
* @version 1.0
* @description: 死信队列 实战
* 消费者 2
* @date 2022/4/23 0023 15:23
*/
public class Consumer02 {
//普通队列名称
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws IOException, TimeoutException {
//通过工具类获取信道
Channel channel = RabbitUtils.getChannel();
System.out.println("等待接收消息·····");
//消息消费成功之后的回调
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("Consumer02接受的消息为:"+new String(message.getBody(),"UTF-8"));
};
channel.basicConsume(DEAD_QUEUE,true,deliverCallback,consumer->{});
}
}
生产者:
package com.example.eight;
import com.example.utils.RabbitUtils;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author 且听风吟
* @version 1.0
* @description: 死信队列 之 生产者
* @date 2022/4/23 0023 21:07
*/
public class Producer {
//普通交换机名称
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitUtils.getChannel();
//死信消息,设置TTL时间为10s
AMQP.BasicProperties properties =
new AMQP.BasicProperties()
.builder().expiration("10000").build();
//发消息
for (int i=1;i<11;i++){
String message = "msg"+i;
channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties,message.getBytes("UTF-8"));
}
}
}
-
先运行消费者 C1 的代码,生成队列
-
再将消费者 C1 关闭,运行生产者,生产者会发送10条消息。但是由于此时消费者C1关闭,无法接受消息,而这些消息又设置了过期时间,所以十秒过后这些消息就会变成死信。通过RabbitMQ客户端可以查看10条死信在 死信队列 中(dead_queue):
-
再运行消费者 C2 的代码,由于消费者 C2 连接了 死信队列,所以会接收到 死信:
6.3.3 队列达到最长长度
在这里将模拟 队列达到最长长度时 消息成为死信的情况
-
修改消费者 C1 代码,添加代码,设置队列长度的限制
此处设置超出6个消息的 消息成为死信
//设置队列长度的限制 var5.put("x-max-length",6);
-
将生产者代码中 设置TTL时间 的代码部分注释掉
//死信消息,设置TTL时间为10s /*AMQP.BasicProperties properties = new AMQP.BasicProperties() .builder().expiration("10000").build();*/ //发消息 for (int i=1;i<11;i++){ String message = "msg"+i; channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",null,message.getBytes("UTF-8")); }
-
由于需要设置队列长度,要运行消费者C1代码,就会生成队列,而再上一节中已经生成队列,再生成会报错,所以需要在控制台删除掉普通队列
-
先启动消费者 C1 代码,然后为了让消息挤压,使队列长度超过设置的默认长度,将 C1 关闭。再运行生产者,发送十条消息。
由于设置了队列长度为6,此时消费者C1关闭,10条消息全部积压在队列当中,会有4条消息 由于超过了队列最大长度成为死信 ,存放在死信队列中。有6条消息存放在普通队列中:
6.3.4 消息被拒绝
在消费者 C1 中设置被拒绝的消息,再次发消息时这些被拒绝的消息会成为死信
-
将之前的普通队列 normal_queue 删除
-
修改 消费者C1的代码,注释掉上一节中 设置队列长度限制 的代码;设置拒绝的消息 并 开启手动应答
//设置队列长度的限制 //var5.put("x-max-length",6); channel.queueDeclare(NORMAL_QUEUE,false,false,false,var5); //声名死信队列 channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
//消息消费成功之后的回调 DeliverCallback deliverCallback = (consumerTag,message)->{ String msg = new String(message.getBody(), "UTF-8"); if (msg.equals("msg4")){ //拒绝消息 msg4 System.out.println("此消息被拒绝:"+msg); //不将此消息放回队列 此消息成为死信 channel.basicReject(message.getEnvelope().getDeliveryTag(),false); }else { System.out.println("Consumer01接受的消息为:"+msg); //应答消息 参数 1:消息内容 2:是否批量应答 channel.basicAck(message.getEnvelope().getDeliveryTag(),false); } }; //第二个参数改为false,表示开启手动应答 channel.basicConsume(NORMAL_QUEUE,false,deliverCallback,consumer->{});
-
重新启动消费者 C1 代码,再启动生产者。可以看到有消息被拒绝了,没有成功回调,成为了死信。此时启动连接 死信队列 的 消费者 C2,可以看到死信被消费者C2接收