RabbitMQ——死信

196 阅读6分钟

死信(Dead Letter)

死信,顾名思义就是无法被消费的消息,字面意思可以这样理 解,一般来说,producer 将消息投递到 broker 或者直接到queue 里了,consumer 从 queue 取出消息 进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有 后续的处理,就变成了死信,有死信自然就有了死信队列。

应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息 消费发生异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时 间未支付时自动失效

产生场景

  • 消费者对消息的negatively acknowledge,如消费者对消息的NACK或reject,并且requeue设置为false
  • 消息过期(TTL,Time To Live)
  • 当消费者队列占满,消息被抛弃

注意:如果是队列设置的x-expires超了,不会导致队列中的消息成为死信

死信交换机(DLX)也是普通的交换机,适用前面的交换机的配置

image-20221007164216498

死信交换机可以由客户端通过参数定义,也可以服务端通过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-key

    args.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();
        }
    }
}

队列及交换机初始化后将得到如下结果:

image-20221008190603259

image-20221008190626300

接下来,启动Publisher,消息将首先发送到normal-consumer队列,等待TTL20S之后消息将成为死信,从而进入死信交换机,根据routing-key分发给死信队列dead-queue

TTL(Time To Live)

TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。

设置TTL

有两种设置TTL的方式,分别是

  1. 为每个消息单独设置TTL

    AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("20000").build();
    channel.basicPublish(NORMAL_EXCHANGE, "live", properties, "RabbitMQ".getBytes(StandardCharsets.UTF_8));
    
  2. 定义队列消息的TTL

    map.put("x-message-ttl", 2000);
    channel.queueDeclare(NORMAL_CONSUMER, false, false, false, map);
    

这里需要注意一下,为队列中的消息定义TTL与为队列设置过期时间是不同的,为队列设置过期时间,当队列过期之后队列以及队列中的所有消息将被直接删除,不会成为死信。

区别

如果是为队列中的消息设置的过期时间,则当消息过期后将会直接被抛弃,直接成为死信

而如果是为每条消息单独设置的过期时间,当队列中某一条消息过期时,并不会立即成为死信。只有当这条消息到达队列头时,才会真正被抛弃。可以理解为当Consumer需要先去竞争消息,当竞争到这条消息之后才会去检查它是否已经过期。

延迟队列

延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望 在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。

使用场景

  1. 订单在十分钟之内未支付则自动取消
  2. 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
  3. 用户注册成功后,如果三天内没有登陆则进行短信提醒。
  4. 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
  5. 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议

实现方式

延时队列目前有两种实现方式,一种是通过死信队列实现,另一种是安装**[rabbitmq_delayed_message_exchange插件](rabbitmq/rabbitmq-delayed-message-exchange: Delayed Messaging for RabbitMQ (github.com))**

通过死信队列实现又有可以分为两种方式,一种是为队列设置TTL,而另一种则是为每条消息单独设置TTL。

为队列设置TTL时有一个问题:每增加一个新的时间需求,就要新增一个队列

而为每条消息单独设置TTL又会有新的问题: RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列, 如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。