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

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

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

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

  • 消息重试:将死信消息重新发送到原队列或者另一个队列进行重试处理
  • 消息丢弃:直接丢弃这些无法处理的消息,以避免它们占用系统资源
  • 日志收集:将死信消息作为日志收集起来,用于后续分析和问题定位
相关推荐
初次攀爬者1 天前
RabbitMQ的消息模式和高级特性
后端·消息队列·rabbitmq
初次攀爬者3 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
让我上个超影吧4 天前
消息队列——RabbitMQ(高级)
java·rabbitmq
塔中妖4 天前
Windows 安装 RabbitMQ 详细教程(含 Erlang 环境配置)
windows·rabbitmq·erlang
断手当码农4 天前
Redis 实现分布式锁的三种方式
数据库·redis·分布式
初次攀爬者4 天前
Redis分布式锁实现的三种方式-基于setnx,lua脚本和Redisson
redis·分布式·后端
业精于勤_荒于稀4 天前
物流订单系统99.99%可用性全链路容灾体系落地操作手册
分布式
Ronin3054 天前
信道管理模块和异步线程模块
开发语言·c++·rabbitmq·异步线程·信道管理
Asher05094 天前
Hadoop核心技术与实战指南
大数据·hadoop·分布式
凉凉的知识库4 天前
Go中的零值与空值,你搞懂了么?
分布式·面试·go