一、什么是死信
无法被消费的消息就称作死信。在RabbitMQ中,生产者将消息投递到 broker 中或者直接投递到 queue 中,消费者从 queue 中拉取消息进行消费。但是由于某些异常情况下 queue 中的消息一致无法被消费者消费,这样的消息如果没有特殊的处理,它就是死信,有死信就衍生出死信队列。
二、如何产生死信
- 消息设置过期时间:TTL
- 消息队列设置了最大长度(队列满了,生产者发送的消息数据就无法添加到队列中去)
- 消息被拒绝
三、实战死信队列
死信队列过程说明
-
消息生产者
TtlProducer
-
正常队列交换机
exchange.topic.ttl.test
-
消息队列
normal.queue
-
正常队列消费者
TtlConsumer
-
死信队列交互
exchange.topic.dead.letter
-
死信队列
dead.queue
-
死信队列消费者
DeadLetterConsumer
1. 消息过期时成为死信
1.1 消息生产者 TtlProducer
public class TtlProducer {
private static String exchangeName = "exchange.topic.ttl.test";
public static void main(String[] args) throws Exception {
Channel channel = RabbitChannelUtil.getChannel();
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, false, false, null);
// 设置消息过期时间为 10 秒
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
String body = "it's ttl message";
String normalRoutingKey = "rk.normal.queue";
for (int i = 1; i <= 10; i++) {
String msg = i + ".".concat(body);
channel.basicPublish(exchangeName, normalRoutingKey, properties, msg.getBytes(StandardCharsets.UTF_8));
}
System.out.println("TtlProducer done");
}
}
1.2 正常队列消费者 TtlConsumer
public class TtlConsumer {
// 普通交换机
private static String exchangeName = "exchange.topic.ttl.test";
// 死信交换机
private static String deadLetterExchange = "exchange.topic.dead.letter";
public static void main(String[] args) throws Exception {
Channel channel = RabbitChannelUtil.getChannel();
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);
channel.exchangeDeclare(deadLetterExchange, BuiltinExchangeType.TOPIC);
String deadQueueName = "dead.queue";
String deadRoutingKey = "rk.dead.queue";
// 声明死信队列
channel.queueDeclare(deadQueueName, false, false, false, null);
// 死信队列绑定到死信交换机的 routingKey
channel.queueBind(deadQueueName, deadLetterExchange, deadRoutingKey);
Map<String, Object> params = new HashMap<>();
//正常队列设置死信交换机
params.put("x-dead-letter-exchange", deadLetterExchange);
//正常队列设置死信 routing-key
params.put("x-dead-letter-routing-key", deadRoutingKey);
String normalQueueName = "normal.queue";
String normalRoutingKey = "rk.normal.queue";
channel.queueDeclare(normalQueueName, false, false, false, params);
channel.queueBind(normalQueueName, exchangeName, normalRoutingKey);
System.out.println("TtlConsumer 等待接收消息......");
// 模拟消费异常情况
System.out.println(1 / 0);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
System.out.println("TtlConsumer 接收到消息:" + new String(body) + ",然后抛出异常!!!");
}
};
channel.basicConsume(normalQueueName, true, consumer);
}
}
1.3 死信队列消费者 DeadLetterConsumer
public class DeadLetterConsumer {
private static String deadQueueName = "dead.queue";
// 死信交换机
private static String deadLetterExchange = "exchange.topic.dead.letter";
public static void main(String[] args) throws Exception {
Channel channel = RabbitChannelUtil.getChannel();
channel.exchangeDeclare(deadLetterExchange, BuiltinExchangeType.TOPIC);
String deadRoutingKey = "rk.dead.queue";
channel.queueDeclare(deadQueueName, false, false, false, null);
channel.queueBind(deadQueueName, deadLetterExchange, deadRoutingKey);
System.out.println("DeadLetterConsumer 等待接收消息......");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("DeadLetterConsumer 接受到消息 body : " + new String(body));
}
};
channel.basicConsume(deadQueueName, true, consumer);
}
}
依次运行 TtlConsumer
、TtlProducer
和 DeadLetterConsumer
,在 RabbitMQ 后台会看到
-
TtlConsumer
启动后,normal.queue 和 dead.queue 中都没有消息 -
TtlProducer
启动后,normal.queue 中有 10 条消息没有被消费 -
10 秒过后看到 dead.queue 中有十条消息
-
DeadLetterConsumer
启动后消息被正常消费掉
2. 消息数量达到了队列最大长度成为死信
在队列 normal.queue
声明时添加 x-max-length
来限制队列的最大长度,核心代码如下:
2.1 消息生产者 MaxLengthProducer
public class MaxLengthProducer {
private static String exchangeName = "exchange.topic.ttl.test";
public static void main(String[] args) throws Exception {
Channel channel = RabbitChannelUtil.getChannel();
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, false, false, null);
String body = "it's ttl message";
String normalRoutingKey = "rk.normal.queue";
for (int i = 1; i <= 10; i++) {
String msg = i + ".".concat(body);
channel.basicPublish(exchangeName, normalRoutingKey, null, msg.getBytes(StandardCharsets.UTF_8));
}
System.out.println("TtlProducer done");
}
}
2.2 正常队列消费者 MaxLengthConsumer
public class MaxLengthConsumer {
// 普通交换机
private static String exchangeName = "exchange.topic.ttl.test";
// 死信交换机
private static String deadLetterExchange = "exchange.topic.dead.letter";
public static void main(String[] args) throws Exception {
Channel channel = RabbitChannelUtil.getChannel();
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);
channel.exchangeDeclare(deadLetterExchange, BuiltinExchangeType.TOPIC);
String deadQueueName = "dead.queue";
String deadRoutingKey = "rk.dead.queue";
// 声明死信队列
channel.queueDeclare(deadQueueName, false, false, false, null);
// 死信队列绑定到死信交换机的 routingKey
channel.queueBind(deadQueueName, deadLetterExchange, deadRoutingKey);
Map<String, Object> params = new HashMap<>();
//正常队列设置死信交换机
params.put("x-dead-letter-exchange", deadLetterExchange);
//正常队列设置死信 routing-key
params.put("x-dead-letter-routing-key", deadRoutingKey);
// 设置正常队列的长度限制
params.put("x-max-length", 6);
String normalQueueName = "normal.queue";
String normalRoutingKey = "rk.normal.queue";
channel.queueDeclare(normalQueueName, false, false, false, params);
channel.queueBind(normalQueueName, exchangeName, normalRoutingKey);
System.out.println("TtlConsumer 等待接收消息......");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
System.out.println("TtlConsumer 接收到消息:" + new String(body));
}
};
channel.basicConsume(normalQueueName, true, consumer);
}
}
2.3 死信队列消费者 DeadLetterConsumer
依次运行 MaxLengthProducer
、MaxLengthConsumer
和 DeadLetterConsumer
,运行之前到RabbtiMQ后台删除normal.queue
队列, 可看到如下运行结果:
- 启动
MaxLengthProducer
MaxLengthConsumer
消费了6条消息
DeadLetterConsumer
消费了4条消息
3. 拒绝消息后成为死信
实现该功能的重要API是channel.basicReject(envelope.getDeliveryTag(), false)
,方法的第二个参数 requeue 设置成 false 表示拒绝消息重新入队,该队列如果配置了死信交换机,将会将消息发送到死信队列中。
3.1 正常队列消费者 RejectProducer
public class RejectProducer {
private static String exchangeName = "exchange.topic.ttl.test";
public static void main(String[] args) throws Exception {
Channel channel = RabbitChannelUtil.getChannel();
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, false, false, null);
String body = "it's ttl message";
String normalRoutingKey = "rk.normal.queue";
for (int i = 1; i <= 10; i++) {
String msg = i + ".".concat(body);
channel.basicPublish(exchangeName, normalRoutingKey, null, msg.getBytes(StandardCharsets.UTF_8));
}
System.out.println("RejectProducer done");
}
}
3.2 正常队列消费者RejectConsumer
public class RejectConsumer {
// 普通交换机
private static String exchangeName = "exchange.topic.ttl.test";
// 死信交换机
private static String deadLetterExchange = "exchange.topic.dead.letter";
public static void main(String[] args) throws Exception {
Channel channel = RabbitChannelUtil.getChannel();
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC);
channel.exchangeDeclare(deadLetterExchange, BuiltinExchangeType.TOPIC);
String deadQueueName = "dead.queue";
String deadRoutingKey = "rk.dead.queue";
// 声明死信队列
channel.queueDeclare(deadQueueName, false, false, false, null);
// 死信队列绑定到死信交换机的 routingKey
channel.queueBind(deadQueueName, deadLetterExchange, deadRoutingKey);
Map<String, Object> params = new HashMap<>();
//正常队列设置死信交换机
params.put("x-dead-letter-exchange", deadLetterExchange);
//正常队列设置死信 routing-key
params.put("x-dead-letter-routing-key", deadRoutingKey);
String normalQueueName = "normal.queue";
String normalRoutingKey = "rk.normal.queue";
channel.queueDeclare(normalQueueName, false, false, false, params);
channel.queueBind(normalQueueName, exchangeName, normalRoutingKey);
System.out.println("RejectConsumer 等待接收消息......");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
if (message.contains("6")) {
System.out.println("RejectConsumer basicReject 接收到消息:" + message);
// requeue 设置成 false 表示拒绝消息重新入队,该队列如果配置了死信交换机,将会将消息发送到死信队列中
channel.basicReject(envelope.getDeliveryTag(), false);
} else {
System.out.println("RejectConsumer basicAck 接收到消息:" + message);
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 不自动确认,因为我们通过 basicReject 和 basicAck 手动进行了确认
channel.basicConsume(normalQueueName, false, consumer);
}
}
3.3 死信队列消费者 DeadLetterConsumer
依次运行 RejectProducer
、RejectConsumer
和 DeadLetterConsumer
,可得如下结果:
RejectConsumer
消费了 9 条消息,拒绝了第 6 条消息,而DeadLetterConsumer
消费了第 6 条消息