• rabbitmq


    rabbit的三种发送订阅模式

    消息从发送,到消费者接收,会经历多个过程:

    在这里插入图片描述

    其中的每一步都可能导致消息丢失,常见的丢失原因包括:

    • 发送时丢失:
    • 生产者发送的消息未送达exchange
    • 消息到达exchange后未到达queue
    • MQ宕机,queue将消息丢失
    • consumer接收到消息后未消费就宕机

    针对这些问题,RabbitMQ分别给出了解决方案。也就是面试题常问的如何rabbitmq如何保证消息的可靠性?

    1. 生产者确认机制
    2. mq持久化
    3. 消费者确认机制
    4. 失败重试机制

    生产者确认机制(解决生产者->交换机->队列)

    步骤
    1.修改生产者配置

    spring:
      rabbitmq:
        publisher-confirm-type: correlated 
        publisher-returns: true
        template:
          mandatory: true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • publish-confirm-type:开启publisher-confirm,这里支持两种类型:
    • simple:同步等待confirm结果,直到超时
    • correlated:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
    • publish-returns:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback
    • template.mandatory:定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息

    2.定义回调函数(每个RabbitTemplate只能配置一个ReturnCallback)
    消息从交换机到队列的过程不通就会回调这个函数

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.BeansException;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.context.annotation.Configuration;
    
    @Slf4j
    @Configuration
    public class CommonConfig implements ApplicationContextAware {
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            // 获取RabbitTemplate
            RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
            // 设置ReturnCallback
            rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
                // 投递失败,记录日志
                log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
                         replyCode, replyText, exchange, routingKey, message.toString());
                // 如果有业务需要,可以重发消息
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    3.定义ConfirmCallback 消息每次发送前都要写

    public void testSendMessage2SimpleQueue() throws InterruptedException {
        // 1.消息体
        String message = "hello, spring amqp!";
        // 2.全局唯一的消息ID,需要封装到CorrelationData中
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        // 3.添加callback
        correlationData.getFuture().addCallback(
            result -> {
                if(result.isAck()){
                    // 3.1.ack,消息成功
                    log.debug("消息发送成功, ID:{}", correlationData.getId());
                }else{
                    // 3.2.nack,消息失败
                    log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
                }
            },
            ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
        );
        // 4.发送消息
        rabbitTemplate.convertAndSend("task.direct", "task", message, correlationData);
    
        // 休眠一会儿,等待ack回执
        Thread.sleep(2000);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    mq持久化(推荐惰性队列)

    • 交换机持久化

    • 队列持久化

    • 消息持久化

    • 交换机持久化

    @Bean
    public DirectExchange simpleExchange(){
        // 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
        return new DirectExchange("simple.direct", true, false);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 队列持久化
    @Bean
    public Queue simpleQueue(){
        // 使用QueueBuilder构建队列,durable就是持久化的
        return QueueBuilder.durable("simple.queue").build();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    消息持久化
    发送时,可以设置消息的属性(MessageProperties),指定delivery-mode
    但是这三种都不是很好的方式

    惰性队列:从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的概念,也就是惰性队列。惰性队列的特征如下:

    • 接收到消息后直接存入磁盘而非内存
    • 消费者要消费消息时才会从磁盘中读取并加载到内存
    • 支持数百万条的消息存储
    • 在这里插入图片描述

    消费者确认机制

    直接配置即可

    spring:
      rabbitmq:
        listener:
          simple:
            acknowledge-mode: manual # 关闭ac
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • none模式下,消息投递是不可靠的,可能丢失
    • auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack
    • manual:自己根据业务情况,判断什么时候该ack

    一般,我们都是使用默认的auto即可。

    手动ack

        @RabbitListener(queues = "fanout.queue1")
    public void onMessage(Message message, Channel channel) throws Exception {
        try {
            // 处理消息
            processMessage(message);
            // 手动确认消息已经被处理完成
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            // 发生异常时可以选择拒绝消息,重新放回队列或者进入死信队列等
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    失败重试机制

    1.生产者发送消息失败配置
    默认是消息重新回到队列,但是如果一直失败,就会一直循环的失败回到队列再重试,频率太高太消耗资源了

    可以本地重试也就是重写发送消息

    spring:
      rabbitmq:
        listener:
          simple:
            retry:
              enabled: true # 开启消费者失败重试
              initial-interval: 1000 # 初识的失败等待时长为1秒
              multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
              max-attempts: 3 # 最大重试次数
              stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    经典面试题:如何保证消息不会重复消费呢?
    1.消费端手动ACK确认机制
    在消费端使用RabbitMQ提供的手动ACK确认机制,在消费者成功处理消息后,手动将消息从队列中删除。这样可以确保消息只会被处理一次,避免了重复消费的问题。
    2.消费端去重保证机制
    可以在消费端处理每条消息之前,通过分布式锁或者数据库唯一索引等方式,判断当前消息是否已经被处理过。如果已经处理过,则忽略该消息;否则正常处理,并将消息标记为已处理。

    死信队列

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

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

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

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

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

    这样才能确保投递的消息能到达死信交换机,并且正确的路由到死信队列。

    在这里插入图片描述

    // 声明普通的 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

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

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

    延迟队列

    概念:延迟队列是一种消息中间件中的特殊队列,用于暂存需要延迟处理的消息,并在一定时间后重新发送到原来的队列或其他队列中进行处理
    延迟消息:是指具有延迟时间属性的消息,在发送时可以设置消息的延迟时间。这个延迟时间表示消息需要在多少时间后才能被消费者消费。
    两者关系:延迟队列是实现延迟消息的一种方式
    定时消息和延迟消息本质上是相同的。它们都根据消息设置的计时时间在固定时间向消费者传递消息。 -----rocketmq官网写的

    允许消息在设置的时间内未被消费,如果到期,未消费发送到死信队列
    模式一:消息本身就可以设置过期时间,设置了过期时间的消息发送到一个没有消费者监听的队列,该队列绑定了死信交换机。当消息过期就会发送到死信交换机,然后再到死信队列。我们只需监听死信队列即可
    上面这种方式不推荐。
    官方推荐
    模式二使用插件,
    RabbitMQ有一个官方的插件社区,地址为:https://www.rabbitmq.com/community-plugins.html
    其中包含各种各样的插件,包括我们要使用的DelayExchange插件:
    大家可以去对应的GitHub页面下载3.8.9版本的插件,地址为https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/tag/3.8.9这个对应RabbitMQ的3.8.5以上版本

    在这里插入图片描述

    惰性队列

    增加队列容量
    从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的概念,也就是惰性队列。惰性队列的特征如下:
    接收到消息后直接存入磁盘而非内存
    消费者要消费消息时才会从磁盘中读取并加载到内存
    支持数百万条的消息存储
    惰性队列是一种用于减少内存占用的优化策略,它的设计目的是在队列中存在大量未消费的消息时,只有当消息被消费者拉取时才将它们加载到内存中。这样可以降低内存使用,并提高系统的性能和吞吐量。
    ————————————————
    版权声明:本文为CSDN博主「自律只猪侠」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/qq_56533553/article/details/131696035

  • 相关阅读:
    Grafana,Loki,Tempo,Prometheus,Agent搭建日志链路监控平台
    【Mysql性能优化系列】查询执行计划EXPLAIN使用详解
    【信号去噪】基于麻雀算法优化VMD实现信号去噪附matlab代码
    算法基础——二分检索
    【走进Linux的世界】Linux---基本指令(1)
    基于微信小程序的付费自习室
    CAD快捷键——标注类
    crAPI靶场学习记录
    解决“您在 /var/spool/mail/root 中有新邮件”问题
    10.12广州见 | 第十六届智慧城市大会报名通道全面开启
  • 原文地址:https://blog.csdn.net/qq_56533553/article/details/133777254