死信(Dead Letter)
死信,顾名思义就是无法被消费的消息,字面意思可以这样理 解,一般来说,producer 将消息投递到 broker 或者直接到queue 里了,consumer 从 queue 取出消息 进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有 后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息 消费发生异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时 间未支付时自动失效
产生场景
- 消费者对消息的negatively acknowledge,如消费者对消息的NACK或reject,并且
requeue设置为false - 消息过期(TTL,Time To Live)
- 当消费者队列占满,消息被抛弃
注意:如果是队列设置的x-expires超了,不会导致队列中的消息成为死信
死信交换机(DLX)也是普通的交换机,适用前面的交换机的配置

死信交换机可以由客户端通过参数定义,也可以服务端通过Polices配置(recomend)
-
使用Policies:
rabbitmqctl set_policy DLX ".*" '{"dead-letter-exchange":"my-dlx"}' --apply-to queues上面的配置中,将死信交换机(
my-dlx)应用到所有队列中,这只作为一个示例,实际场景中,可以使用dead-letter-routing-key来配置死信交换机的routing-key -
使用参数定义:
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将死信交换机和普通队列绑定,也可以使用如下代码设置死信消息的routing-key,如果这个参数未被设置,将使用消息本身成为死信前的routing-keyargs.put("x-dead-letter-routing-key", "some-routing-key");
安全性
默认情况下,死信消息会被重新发布并且内部不会有发布确认。因此,在集群RabbitMQ环境中使用DLX并不能保证安全。消息发布到DLX目标队列后立即从原始队列中删除。这确保了不会出现过多的消息累积,以免耗尽代理资源,但是如果目标队列不可用,则消息可能丢失。
从RabbitMQ3.10开始,通过at least once dead lettering来支持死信消息重发时的消息确认
示例代码
将用示例演示TTL超时的情况说明死信队列,先初始化Consumer的信道,再关闭Consumer客户端
在Consumer中,需要定义正常的消费队列,将队列与发布交换机绑定,并与死信交换机绑定
此处代码为了方便起见,将死信交换机与死信队列的声明放入Consumer中
Consumer代码如下:
public class Consumer1 {
public static final String DEAD_QUEUE = "dead-queue";
public final static String DEAD_EXCHANGE = "dead-exchange";
public final static String NORMAL_CONSUMER = "normal-consumer";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// dead letter exchange
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
// declare dead letter queue
channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
// bind the dead letter queue and exchange
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "dead");
// declare and bind (normal consumer queue and dead letter exchange)
// bind dead letter exchange to normal consumer queue
HashMap<String, Object> map = new HashMap<>(2);
map.put("x-dead-letter-exchange", DEAD_EXCHANGE);
map.put("x-dead-letter-routing-key", "dead");
channel.queueDeclare(NORMAL_CONSUMER, false, false, false, map);
// bind to normal exchange
channel.queueBind(NORMAL_CONSUMER, Publisher.NORMAL_EXCHANGE, "live");
// recive the message and exit, simulate the ttl situation
DeliverCallback deliverCallback = (consumerTag, delivery) ->
{
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("Consumer01 接收到消息" + message);
System.exit(0);
};
channel.basicConsume(NORMAL_CONSUMER, true, deliverCallback, consumerTag -> {
});
}
}
Publisher中只需定义发布的Exchange
public class Publisher {
public static final String NORMAL_EXCHANGE = "normal-exchange";
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection(); Channel channel = connection.createChannel()){
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("20000").build();
for (int i = 0; i < 11; i++) {
String msg = "MESSAGE " + i;
channel.basicPublish(NORMAL_EXCHANGE, "live", properties, msg.getBytes(StandardCharsets.UTF_8));
}
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
}
队列及交换机初始化后将得到如下结果:


接下来,启动Publisher,消息将首先发送到normal-consumer队列,等待TTL20S之后消息将成为死信,从而进入死信交换机,根据routing-key分发给死信队列dead-queue
TTL(Time To Live)
TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。
设置TTL
有两种设置TTL的方式,分别是
-
为每个消息单独设置TTL
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("20000").build(); channel.basicPublish(NORMAL_EXCHANGE, "live", properties, "RabbitMQ".getBytes(StandardCharsets.UTF_8)); -
定义队列消息的TTL
map.put("x-message-ttl", 2000); channel.queueDeclare(NORMAL_CONSUMER, false, false, false, map);
这里需要注意一下,为队列中的消息定义TTL与为队列设置过期时间是不同的,为队列设置过期时间,当队列过期之后队列以及队列中的所有消息将被直接删除,不会成为死信。
区别
如果是为队列中的消息设置的过期时间,则当消息过期后将会直接被抛弃,直接成为死信
而如果是为每条消息单独设置的过期时间,当队列中某一条消息过期时,并不会立即成为死信。只有当这条消息到达队列头时,才会真正被抛弃。可以理解为当Consumer需要先去竞争消息,当竞争到这条消息之后才会去检查它是否已经过期。
延迟队列
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望 在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
使用场景
- 订单在十分钟之内未支付则自动取消
- 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
- 用户注册成功后,如果三天内没有登陆则进行短信提醒。
- 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
- 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
实现方式
延时队列目前有两种实现方式,一种是通过死信队列实现,另一种是安装**[rabbitmq_delayed_message_exchange插件](rabbitmq/rabbitmq-delayed-message-exchange: Delayed Messaging for RabbitMQ (github.com))**
通过死信队列实现又有可以分为两种方式,一种是为队列设置TTL,而另一种则是为每条消息单独设置TTL。
为队列设置TTL时有一个问题:每增加一个新的时间需求,就要新增一个队列
而为每条消息单独设置TTL又会有新的问题: RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列, 如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。