1 缘起
接上两篇:
RabbitMQ优先级队列
RabbitMQ惰性/延迟队列
继续补充RabbitMQ队列知识,
啥是死信?
即无法被某个原生队列消费的消息
(可能由于消息过期、拒绝消费和达到队列存储上限产生),
但是,消息也不应该不处理直接丢弃,
因此,统一将该类信息转储到死信队列,
当然,不是天然就可以转储的,需要为原生队列配置死信相关信息,如死信交换器、死信路由键,
最重要,是保证存储死信的队列正常工作。
2 死信架构
死信架构如下图所示,
由图可知,默认队列default-queue正常入队和出队消息,
当入队和出队发生消息成为死信的状况时,
会将该死信消息发送到为队列default-queue配置的存储死信消息的队列:dlx-queue,
死信队列、路由键和队列是独立于正常队列的一套完整入队出队系统,
只不过该系统专门用于处理死信(消息)。
3 死信交换器
队列的消息可以为“死信”,正常情况下,“死信”会从正常的队列中移除,进入专门的“死信”队列,
以下情况会将消息重发到死信交换器:
- 消费者使用basic.reject或者basic.nack配置requeue参数为false;
- 队列中消息过期(TTL);
- 消息数达到队列长度上限被丢弃。
注意:过期的队列不会使其存储的消息成为死信。
死信交换器(DLX)是普通的交换器,可以随意使用,对于任何队列,DLX可以通过队列参数配置,或者使用Policy设置(运行时)。
3.1 通过Policy配置(运行时)
队列可以通过Policy配置死信交换器属性,
Linux和Windows客户端命令如下:
为队列dead-letter-queue添加死信交换器,改队列的数据成为死信后,
会由交换器my-dlx-ex发送到指定队列。
- Linux操作系统
rabbitmqctl set_policy DLX "^dead-letter-queue$" '{"dead-letter-exchange":"my-dlx-ex"}' --vhost /tutorial --apply-to queues
- Windows操作系统
rabbitmqctl set_policy DLX "^dead-letter-queue$" "{""dead-letter-exchange"":""my-dlx-ex""}" --vhost /tutorial --apply-to queues
3.2 通过参数配置(声明时)
通过参数亦可以在声明队列时指定死信交换器。
新建队列myqueue,声明死信some.exchange.name交换器。
声明队列时可以不必声明交换器,但是,当消息为死信时,交换器应该就绪(存在),
如果交换器不存在,消息会被悄悄滴删除。
配置代码段如下。
channel.exchangeDeclare("some.exchange.name", "direct");
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-dead-letter-exchange", "some.exchange.name");
channel.queueDeclare("myqueue", false, false, false, args);
发送死信时可以指定路由键,如果路由键未设定,则使用消息自身的路由键。
指定死信队列并且声明队列配置权限时,用户需要有读写交换器的权限,
args.put("x-dead-letter-routing-key", "some-routing-key");
3.3 路由死信消息
死信消息会路由到死信交换器,如:
- 队列指定了路由键
- 使用与最初发布相同的路由键
比如使用路由键foo通过交换器向队列发送消息,消息为死信时,消息会通过路由键foo发送到死信交换器。如果队列消息最初指定的死信路由键x-dead-letter-routing-key为bar,死信消息会通过路由键bar发送到死信交换器。
3.4 安全性
默认情况下,死信消息是重新发布的,发布者不会进行发布确认,因此,RabbitMQ集群环境中使用DLX不能保证是安全的。当消息发送到DLX绑定的队列后,消息会会立即从原始队列移除。这样虽然可以确保频繁的消息创建不会耗尽Broker资源,但是,消息有可能丢失(目标队列无法接收消息时)。
3.5 死信对消息的影响
死信消息修改头:
- 交换器名称被最新的死信交换器替换
- 路由键被指定队列的死信替换
死信进程为每个死信消息添加一个x-death的数组,该数组包含每个死信事件的实体,通过{queue, reason}标识,每个实体包含的内容有:
序号 | 参数 | 描述 |
---|---|---|
1 | queue | 消息成为死信前所在队列的队列名称 |
2 | reason | 消息成为死信的原因 |
3 | time | 消息成为死信的日期和时间 |
4 | exchange | 消息发送到的交换器(如果消息有多个死信时间,交换器为死信交换器) |
5 | routing-keys | 消息发送的路由键 |
6 | count | 当前死信原因中,消息成为死信的次数 |
7 | original-expiration | 如果消息成为死信是由于消息过期,消息原始过期属性,移除过期属性防止在死信队列中再次过期 |
新版本中增加了新的x-death属性。reason属性细分了死信原因:
- rejected:消息由requeue参数设置为false而拒绝
- expired:消息过期
- maxlen:消息数量达到队列上限
- delivery_limit:消息返回次数超过上限
死信消息的头前三级为:x-first-death-reason、x-first-death-queue和x-first-death-exchange。
4 Code实践
实践部分,给出4种消息成为死信的样例。
- 消息过期;
- 队列消息达到设定上限;
- basic.reject配置requeue为fasle;
- basic.nack配置requeue为false;
4.1 消息过期进入死信
通过x-message-ttl配置队列消息过期时间,单位为:毫秒,
这里设置消息过期时间为20秒,
建立队列并填入数据,
测试样例如下:
package com.monkey.java_study.mq.rabbitmq;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* RabbitMQ DLX生产者:消息过期.
*
* @author xindaqi
* @since 2022-07-29 12:11
*/
public class RabbitMQDlxTtlProvider {
public static void main(String[] args) {
try (Connection connection = RabbitMQConnection.connectionFactoryDefault(); Channel channel = connection.createChannel()) {
// 通用配置
boolean durableFlag = true;
boolean exclusiveFlag = false;
boolean autoDeleteFlag = false;
// DLX配置
String dlxQueue = "dlx-queue";
String dlxExchange = "dlx-exchange";
String dlxRoutingKey = "dlx-routing-key";
channel.queueDeclare(dlxQueue, durableFlag, exclusiveFlag, autoDeleteFlag, null);
channel.exchangeDeclare(dlxExchange, BuiltinExchangeType.DIRECT);
channel.queueBind(dlxQueue, dlxExchange, dlxRoutingKey);
// 普通Queue配置
String defaultQueue = "default-queue";
String defaultExchange = "default-exchange";
String defaultRoutingKey = "default-routing-key";
Map<String, Object> defaultArgs = new HashMap<>(4);
// 消息20秒过期
defaultArgs.put("x-message-ttl", 20000);
// 指定死信交换器
defaultArgs.put("x-dead-letter-exchange", dlxExchange);
// 指定死信路由键
defaultArgs.put("x-dead-letter-routing-key", dlxRoutingKey);
channel.queueDeclare(defaultQueue, durableFlag, exclusiveFlag