• RabbitMQ-死信交换机


    1 死信交换机

    1.1.什么是死信交换机

    什么是死信?

    当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):

    • 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
    • 消息是一个过期消息,超时无人消费
    • 要投递的队列消息满了,无法投递

    如果这个包含死信的队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,检查DLX)。

    如图,一个消息被消费者拒绝了,变成了死信:
    在这里插入图片描述
    因为simple.queue绑定了死信交换机 dl.direct,因此死信会投递给这个交换机:
    在这里插入图片描述
    如果这个死信交换机也绑定了一个队列,则消息最终会进入这个存放死信的队列:
    在这里插入图片描述

    另外,队列将死信投递给死信交换机时,必须知道两个信息:

    • 死信交换机名称
    • 死信交换机与死信队列绑定的RoutingKey

    这样才能确保投递的消息能到达死信交换机,并且正确的路由到死信队列
    在这里插入图片描述

    1.2 利用死信交换机接收死信

    在失败重试策略中,默认的RejectAndDontRequeueRecoverer会在本地重试次数耗尽后,发送reject给RabbitMQ,消息变成死信,被丢弃。

    我们可以给simple.queue添加一个死信交换机,给死信交换机绑定一个队列。这样消息变成死信后也不会丢弃,而是最终投递到死信交换机,路由到与死信交换机绑定的队列。

    我们在consumer服务中,定义一组死信交换机、死信队列:

    // 声明普通的 simple.queue队列,并且为其指定死信交换机:dl.direct
    @Bean
    public Queue simpleQueue2(){
        return QueueBuilder.durable("simple.queue") // 指定队列名称,并持久化
            .deadLetterExchange("dl.direct") // 指定死信交换机
            .build();
    }
    // 声明死信交换机 dl.direct
    @Bean
    public DirectExchange dlExchange(){
        return new DirectExchange("dl.direct", true, false);
    }
    // 声明存储死信的队列 dl.queue
    @Bean
    public Queue dlQueue(){
        return new Queue("dl.queue", true);
    }
    // 将死信队列 与 死信交换机绑定
    @Bean
    public Binding dlBinding(){
        return BindingBuilder.bind(dlQueue()).to(dlExchange()).with("simple");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    1.3 总结

    什么样的消息会成为死信?

    • 消息被消费者reject或者返回nack
    • 消息超时未消费
    • 队列满了

    死信交换机的使用场景是什么?

    • 如果队列绑定了死信交换机,死信会投递到死信交换机;
    • 可以利用死信交换机收集所有消费者处理失败的消息(死信),交由人工处理,进一步提高消息队列的可靠性。

    2 TTL

    一个队列中的消息如果超时未消费,则会变为死信,超时分为两种情况:

    • 消息所在的队列设置了超时时间
    • 消息本身设置了超时时间

    在这里插入图片描述

    2.1 接收超时死信的死信交换机

    在consumer服务的SpringRabbitListener中,定义一个新的消费者,并且声明 死信交换机、死信队列:

    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "dl.ttl.queue", durable = "true"),
        exchange = @Exchange(name = "dl.ttl.direct"),
        key = "ttl"
    ))
    public void listenDlQueue(String msg){
        log.info("接收到 dl.ttl.queue的延迟消息:{}", msg);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.2 声明一个队列,并且指定TTL

    要给队列设置超时时间,需要在声明队列时配置x-message-ttl属性:

    @Bean
    public Queue ttlQueue(){
        return QueueBuilder.durable("ttl.queue") // 指定队列名称,并持久化
            .ttl(10000) // 设置队列的超时时间,10秒
            .deadLetterExchange("dl.ttl.direct") // 指定死信交换机
            .build();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意,这个队列设定了死信交换机为dl.ttl.direct

    声明交换机,将ttl与交换机绑定:

    @Bean
    public DirectExchange ttlExchange(){
        return new DirectExchange("ttl.direct");
    }
    @Bean
    public Binding ttlBinding(){
        return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttl");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    发送消息,但是不要指定TTL:

    @Test
    public void testTTLQueue() {
        // 创建消息
        String message = "hello, ttl queue";
        // 消息ID,需要封装到CorrelationData中
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        // 发送消息
        rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData);
        // 记录日志
        log.debug("发送消息成功");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    发送消息的日志:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VilczII5-1669978976776)(assets/image-20210718191657478.png)]

    查看下接收消息的日志:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rk4jWyng-1669978976777)(assets/image-20210718191738706.png)]

    因为队列的TTL值是10000ms,也就是10秒。可以看到消息发送与接收之间的时差刚好是10秒。

    2.3 发送消息时,设定TTL

    在发送消息时,也可以指定TTL:

    @Test
    public void testTTLMsg() {
        // 创建消息
        Message message = MessageBuilder
            .withBody("hello, ttl message".getBytes(StandardCharsets.UTF_8))
            .setExpiration("5000")
            .build();
        // 消息ID,需要封装到CorrelationData中
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        // 发送消息
        rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData);
        log.debug("发送消息成功");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    查看发送消息日志:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xRKftnIo-1669978976778)(assets/image-20210718191939140.png)]

    接收消息日志:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-346X1ExA-1669978976779)(assets/image-20210718192004662.png)]

    这次,发送与接收的延迟只有5秒。说明当队列、消息都设置了TTL时,任意一个到期就会成为死信。

    2.4.总结

    消息超时的两种方式是?

    • 队列设置ttl属性,进入队列后超过ttl时间的消息变为死信
    • 消息设置ttl属性,队列接收到消息超过ttl时间后变为死信

    如何实现发送一个消息20秒后消费者才收到消息?

    • 给消息的目标队列指定死信交换机
    • 将消费者监听的队列绑定到死信交换机
    • 发送消息时给消息设置超时时间为20秒

    3 延迟队列

    利用TTL结合死信交换机,我们实现了消息发出后,消费者延迟收到消息的效果。这种消息模式就称为延迟队列(Delay Queue)模式。

    延迟队列的使用场景包括:

    • 延迟发送短信
    • 用户下单,如果用户在15 分钟内未支付,则自动取消
    • 预约工作会议,20分钟后自动通知所有参会人员

    因为延迟队列的需求非常多,所以RabbitMQ的官方也推出了一个插件,原生支持延迟队列效果。

    这个插件就是DelayExchange插件。参考RabbitMQ的插件列表页面:https://www.rabbitmq.com/community-plugins.html

    在这里插入图片描述

    使用方式可以参考官网地址:https://blog.rabbitmq.com/posts/2015/04/scheduling-messages-with-rabbitmq

    3.1 DelayExchange原理

    DelayExchange需要将一个交换机声明为delayed类型。当我们发送消息到delayExchange时,流程如下:

    • 接收消息
    • 判断消息是否具备x-delay属性
    • 如果有x-delay属性,说明是延迟消息,持久化到硬盘,读取x-delay值,作为延迟时间
    • 返回routing not found结果给消息发送者
    • x-delay时间到期后,重新投递消息到指定队列

    3.2 使用DelayExchange

    插件的使用也非常简单:声明一个交换机,交换机的类型可以是任意类型,只需要设定delayed属性为true即可,然后声明队列与其绑定即可。

    1)声明DelayExchange交换机

    • 基于注解方式(推荐):

    在这里插入图片描述

    • 基于@Bean的方式:

    在这里插入图片描述

    2)发送消息

    发送消息时,一定要携带x-delay属性,指定延迟的时间:

    在这里插入图片描述

    3.3 总结

    延迟队列插件的使用步骤包括哪些?

    • 声明一个交换机,添加delayed属性为true

    • 发送消息时,添加x-delay头,值为超时时间

  • 相关阅读:
    Java并发编程--多线程间的同步控制和通信
    FBA海运到美国费用一般包含哪些?
    京东数据分析:2023年9月京东白酒行业品牌销售排行榜
    CSS面试题---基础
    雷尼绍探头编程 9810
    Perl动态建立ZIP压缩文件
    什么是N卡和A卡?有什么区别?
    centos7安装mysql5.7
    List.of() Vs Arrays.asList()
    网络安全市场投资融资趋势报告
  • 原文地址:https://blog.csdn.net/jhgjyfgyu/article/details/128150391