RabbitMQ_7_高级特性(4)

TTL

TTL(Time to Live,过期时间),RabbitMQ可以对消息和队列设置TTL。

当消息到达存活时间之后哦,还没有被消费,就会被自动清除。

在网购的时候,经常会遇到的一个场景,当下单超过24小时还未付款,订单就会被自动取消,还有类似的,申请退款之后,超过7天未被处理,则自动退款。

设置消息TTL

目前有两种方法可以设置消息的TTL。一是设置队列的TTL,队列中所有消息都有相同的过期时间。二是对消息本身进行单独设置,每条消息的TTL可以不同。如果两种方法一起使用,则消息的TTL以两者之间较小的那个数值为准。

针对每条消息设置TTL

针对每条消息设置TTL的方法是在发送消息的方法中加入expiration属性参数,单位为ms。

配置交换机队列:

java 复制代码
    //未带ttl的队列
    public static final String TTL_QUEUE = "ttl.queue";
    //交换机
    public static final String TTL_EXCHANGE = "ttl.exchange";
java 复制代码
    @Bean("ttlQueue")
    public Queue ttlQueue(){
        return QueueBuilder.durable(Contants.TTL_QUEUE).build();
    }


    @Bean("ttlExchange")
    public DirectExchange ttlExchange(){
        return ExchangeBuilder.directExchange(Contants.TTL_EXCHANGE).build();
    }

    @Bean("ttlBinding")
    public Binding ttlBinding(@Qualifier("ttlQueue")Queue queue,
                              @Qualifier("ttlExchange")DirectExchange ttlExchange){
        return BindingBuilder.bind(queue).to(ttlExchange).with("ttl");
    }

    

发送消息:

java 复制代码
@RequestMapping("/ttl")
    public String ttl(){
       
        rabbitTemplate.convertAndSend(Contants.TTL_EXCHANGE,"ttl","ttl2 test...",message ->             {
            message.getMessageProperties().setExpiration("10000");//单位是毫秒,设置过期时间为10s
            return message;
        });
        return "消息发送成功";
    }

运行程序,观察结果:127.0.0.1:8080/producer/ttl

10秒钟之后,刷新页面,发现消息已经被删除了。

如果不设置TTL,则表示此消息不会过期;如果将TTL设置为0,则表示除非此时可以将消息投递到消费者,否则该消息会立即被丢弃。

设置队列的TTL

设置队列TTL的方法是在创建队列时,加入x-message-ttl 参数实现的,单位是毫秒。

配置队列和绑定关系:

java 复制代码
    public static final String TTL_QUEUE2 = "ttl2.queue";

    public static final String TTL_EXCHANGE = "ttl.exchange";
java 复制代码
@Bean("ttlQueue2")
    public Queue ttlQueue2(){
        return QueueBuilder.durable(Contants.TTL_QUEUE2).ttl(20000).build();//设置队列ttl为20s
    }

@Bean("ttlExchange")
    public DirectExchange ttlExchange(){
        return ExchangeBuilder.directExchange(Contants.TTL_EXCHANGE).build();
    }

@Bean("ttlBinding2")
    public Binding ttlBinding2(@Qualifier("ttlQueue2")Queue queue,
                              @Qualifier("ttlExchange")DirectExchange ttlExchange){
        return BindingBuilder.bind(queue).to(ttlExchange).with("ttl");
    }

设置队列过期时间,也可以采用以下方式:

java 复制代码
    public static final String TTL_QUEUE3 = "ttl3.queue";
java 复制代码
@Bean("ttlQueue3")
    public Queue ttlQueue3(){
        Map<String, Object> map = new HashMap<>();
        map.put("x-message-ttl",20000);
        return QueueBuilder.durable(Contants.TTL_QUEUE3).withArguments(map).build();//设置队列ttl为20s
    }

发送消息:

java 复制代码
@RequestMapping("/ttl2")
    public String ttl2(){
        //发送普通消息
        rabbitTemplate.convertAndSend(Contants.TTL_EXCHANGE,"ttl","ttl test...");
        return "消息发送成功";
    }

运行程序,观察结果:

运行完之后,发现新增了两个队列,队列Features有一个TTL标识:

调用接口,发送消息:127.0.0.1:8080/producer/ttl2

发送消息之后,可以看到ttl1队列和ttl2队列中多了一条消息:

20s之后,刷新页面,ttl2队列中的消息消失,因为ttl队列未设置过期时间,且发送的是普通消息,所以ttl_queue的消息未删除:

两者区别

设置队列TTL属性的方法,一旦消息过期就会从队列中删除。

设置消息TTL的方法,即使消息过期,也不会马上从队列中删除,而是在即将投递到消费者之前进行判定。

为什么这两种方法处理的方式不一样?

因为设置队列过期时间,队列中已过期的消息肯定在队列头部,RabbitMQ只要定期从对头开始扫描是否有过期的消息即可。

而设置消息TTL的方式,因为每条消息的过期时间不同,如果要删除所有过期消息需要扫描整个队列,所以不如等到此消息即将被消费时再判定是否过期,如果过期再进行删除即可。

修改发送消息的代码:

java 复制代码
 @RequestMapping("/ttl")
    public String ttl(){
        rabbitTemplate.convertAndSend(Contants.TTL_EXCHANGE,"ttl","ttl1 test...",message -> {
            message.getMessageProperties().setExpiration("30000");//单位是毫秒,设置过期时间为30s
            return message;
        });
        rabbitTemplate.convertAndSend(Contants.TTL_EXCHANGE,"ttl","ttl2 test...",message -> {
            message.getMessageProperties().setExpiration("10000");//单位是毫秒,设置过期时间为10s
            return message;
        });
        return "消息发送成功";
    }

重启服务器后,访问接口:127.0.0.1:8080/producer/ttl

可以看到我们在ttl中设置过期时间为10s的消息并没有及时被删除,进一步印证了没有设置过期时间的队列会等到消息即将被消费时,再进行判定。

死信队列

死信的概念

死信简单理解就是因为种种原因,无法被消费的消息,就是死信。

有死信,自然就有死信队列。当消息在一个队列中变成死信之后,它能被重新被发送到一个交换机中,这个交换机就是DLX(死信交换机),绑定DLX的队列就称为死信队列(DLQ)。

消息变成死信一般是由于以下几种情况:

  1. 消息被拒绝(手动确认),并且requeue参数为false
  2. 消息过期
  3. 队列达到最大长度

代码演示:

声明队列和交换机:

注意:这里需要声明正常队列和正常交换机,声明死信队列和死信交换机!!!死信交换机和死信队列与普通队列和交换机没有区别

java 复制代码
 //死信
    public static final String NORMAL_QUEUE = "normal.queue";
    public static final String NORMAL_EXCHANGE = "normal.exchange";
    public static final String DL_QUEUE = "dl.queue";
    public static final String DL_EXCHANGE = "dl.exchange";
java 复制代码
@Configuration
public class DLConfig {
    //正常交换机和队列
    @Bean("normalQueue")
    public Queue normalQueue(){
        return QueueBuilder.durable(Contants.NORMAL_QUEUE)
                .deadLetterExchange(Contants.DL_EXCHANGE)
                .deadLetterRoutingKey("dlx")       
                .build();
    }

    @Bean("normalExchange")
    public DirectExchange normalExchange(){
        return ExchangeBuilder.directExchange(Contants.NORMAL_EXCHANGE).build();
    }

    @Bean("normalBinding")
    public Binding normalBinding(@Qualifier("normalQueue") Queue queue,
    @Qualifier("normalExchange")DirectExchange directExchange){
        return BindingBuilder.bind(queue).to(directExchange).with("normal");
    }
    //死信交换机和队列
    @Bean("dlQueue")
    public Queue dlQueue(){
        return QueueBuilder.durable(Contants.DL_QUEUE).build();
    }

    @Bean("dlExchange")
    public DirectExchange dlExchange(){
        return ExchangeBuilder.directExchange(Contants.DL_EXCHANGE).build();
    }

    @Bean("dlBinding")
    public Binding dlBinding(@Qualifier("dlQueue") Queue queue,
                                 @Qualifier("dlExchange")DirectExchange directExchange){
        return BindingBuilder.bind(queue).to(directExchange).with("dlx");
    }
}

发送消息:

java 复制代码
   @RequestMapping("/dl")
    public String dl(){
        //发送普通消息
         rabbitTemplate.convertAndSend(Contants.NORMAL_EXCHANGE,"normal","dl test...");     
        return "消息发送成功";
    }
消息过期的情况

修改队列过期时间为10s:

java 复制代码
    //正常交换机和队列
    @Bean("normalQueue")
    public Queue normalQueue(){
        return QueueBuilder.durable(Contants.NORMAL_QUEUE)
                .deadLetterExchange(Contants.DL_EXCHANGE)
                .deadLetterRoutingKey("dlx")
                .ttl(10000)
                .build();
    }

访问:127.0.0.1:8080/producer/dl

消息拒收的情况

消费者代码:

java 复制代码
@Component
public class DLListener {
    @RabbitListener(queues = Contants.NORMAL_QUEUE)
    public void handleMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            //消费者逻辑
            System.out.printf("[normal.queue]接收到消息:%s,deliveryTag:%d\n", new String(message.getBody()),
                    message.getMessageProperties().getDeliveryTag());
            //进行业务逻辑处理
            System.out.println("模拟业务逻辑处理");
            int num = 3 / 0;
            System.out.println("业务逻辑处理完成");
            //肯定确认
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            //否定确认
            channel.basicNack(deliveryTag, false, false);//requeue为false,该消息变成死信
        }
    }

    @RabbitListener(queues = Contants.DL_QUEUE)
    public void dlHandleMessage(Message message, Channel channel) throws Exception {
        //消费者逻辑
        System.out.printf("[dl.queue]接收到消息:%s,deliveryTag:%d\n", new String(message.getBody()),
                message.getMessageProperties().getDeliveryTag());

    }
}

注意:这里需要修改配置自动确认!!!

访问:127.0.0.1:8080/producer/dl

观察控制台,可以看到当前消息未被正确消费且不进行重新入队的情况下,会被发送到死信队列中:

到达队列长度的情况

修改队列配置,长度为10。注意:这里需要删除老队列,不然会报出异常!!!并且将Listener的内容注掉!!!

java 复制代码
  //正常交换机和队列
    @Bean("normalQueue")
    public Queue normalQueue(){
        return QueueBuilder.durable(Contants.NORMAL_QUEUE)
                .deadLetterExchange(Contants.DL_EXCHANGE)
                .deadLetterRoutingKey("dlx")
                .maxLength(10L)
                .ttl(10000)
                .build();
    }

修改发送消息的代码,发送20条消息:

java 复制代码
    @RequestMapping("/dl")
    public String dl(){
        //测试队列长度
        for (int i = 0; i < 20; i++) {
            rabbitTemplate.convertAndSend(Contants.NORMAL_EXCHANGE,"normal","dl test...");
       }
        return "消息发送成功";
    }
}

可以看到发送消息后,有10条消息因为超过队列长度被放到死信队列中;10s后,又有10条消息因为过期被放到死信队列中。

常见面试题

死信队列的概念

死信是消息队列中的一种特殊消息,它指的是那些无法被正常消费或处理的消息,在消息队列系统中,如RabbitMQ,死信队列用于存储这些死信消息

死信的来源
  1. 消息过期:消息在队列中存活的时间超过了设定的TTL
  2. 消息被拒绝:消费者在处理消息时,可能因为消息内容错误,处理逻辑异常等原因拒绝处理该消息。如果拒绝时指定不重新入队,消息也会成为死信。
  3. 队列满了:当队列达到最大长度,无法再容纳新的消息时,信赖的消息会被处理为死信。
死信队列的应用场景

对于RabbitMQ来说,死信队列时一个非常有用的特性。它可以处理异常情况下,消息不能被消费者正确消费而被置入死信队列中的情况,应用程序可以通过消费这个死信队列中的内容来分析当时所遇到的异常情况,进而可以改善和优化系统。

比如:用户支付订单之后,支付系统会给订单系统返回当前订单的支付状态

为了保证支付信息不丢失,需要使用到死信队列的机制。当消息消费异常时,将消息投入到死信队列中,由订单系统的其他消费者来监听这个队列,并对数据进行处理(比如发送工单等,进行人工确认)。

死信队列的应用场景还有:

  • 消息重试:将死信消息重新发送到原队列或者另一个队列进行重试处理
  • 消息丢弃:直接丢弃这些无法处理的消息,以避免它们占用系统资源
  • 日志收集:将死信消息作为日志收集起来,用于后续分析和问题定位
相关推荐
赵榕2 小时前
RabbitMQ发布订阅模式同一消费者多个实例如何防止重复消费?
分布式·微服务·rabbitmq
古城小栈3 小时前
雾计算架构:边缘-云端协同的分布式 AI 推理
人工智能·分布式·架构
lang201509283 小时前
Kafka高可用:延迟请求处理揭秘
分布式·kafka·linq
库库林_沙琪马3 小时前
5、Seata
分布式·后端
lang201509283 小时前
Kafka副本同步机制核心解析
分布式·kafka·linq
lang201509286 小时前
深入解析Kafka核心:Partition类源码揭秘
分布式·kafka·linq
Blossom.1186 小时前
基于图神经网络+大模型的网络安全APT检测系统:从流量日志到攻击链溯源的实战落地
人工智能·分布式·深度学习·安全·web安全·开源软件·embedding
梦里不知身是客117 小时前
spark如何调节jvm的连接等待时长
大数据·分布式·spark