• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 众包
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
自由的代价是孤独丶
博客园    首页    新随笔    联系   管理    订阅  订阅

RabbitMQ详解

1. RabbitMQ简介

RabbitMQ是由erlang语言开发,基于AMQP(高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法。
RabbitMQ官方地址:http://www.rabbitmq.com




1.1 什么是消息队列

MQ:全称Message Queue,即消息队列。是在消息的传输过程中保存消息的容器。它是典型的:生产者、消费者设计模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。




1.2 消息队列使用场景与优势【异步,解耦,削峰】

  • 1. 异步:
说明:
    高并发环境下,由于来不及同步处理,请求往往会发生堵塞。
    我们可以异步处理请求,从而缓解系统的压力。将不需要同步处理的并且耗时长的操作,由消息队列通知消息接收方进行异步处理。
场景:
    用户注册后,需要发送注册邮件和注册成功短信通知【这两个操作都不是必须的,只是一个通知,没必要让用户等待收到邮箱和短信后才算注册成功】。

引入消息队列后:
        将发送邮件和短信的业务交给消息队列,实现异步处理。




  • 2. 解耦:
说明:
    MQ相当于一个中介,生产方通过MQ与消费方交互,不直接进行交互,它将生产方与消费方进行解耦合,不至于当时功能里面的某一个操作因为宕机了导致后续操作无法进行。
场景:
    双十一购物狂欢节,用户下单后,订单系统需要通知库存系统 减少响应库存量,若库存系统出现故障,此笔订单就不能成功。

引入消息队列后:
        订单系统向消息队列发送用户下单的消息,从而与消费者进行了解耦,消息队列会将数据持久化,库存系统监听消息队列的消息。



  • 3. 削峰:
场景:
    秒杀活动,一般流量会很大,可能导致某个系统直接扛不住而挂掉。
    
引入消息队列后:
        用户发起请求时,先来到消息队列再去秒杀系统。在消息队列中对消息进行处理(比如请求量达到消息队列阈值时,直接抛弃那些请求或跳转错误页面),如此一来可缓解因高并发请求所导致秒杀系统扛不住挂掉的问题。




1.3 消息队列的劣势

  • 系统可用性降低:
    系统依赖了MQ,若MQ宕机,就会对业务造成影响。要考虑如何保证MQ的高可用。

  • 系统复杂度提高:
    使用MQ进行异步调用后,如何保证消息没有被 1、MQ重复消费? 2、保证消息可靠性? 3、怎么保证消息传递的顺序性? 4、消息积压问题?

  • 一致性问题:
    A系统处理完业务,通过MQ给 B,C,D 三个系统发消息数据,若B,C系统处理成功,D系统处理失败,消息被如何多个消费者消费时的事务问题?



1.4 想使用MQ,你的功能需要满足什么条件

    1. 生产者不需要从消费者处获得反馈就能完成该功能的处理。
    2. 容许短暂的不一致性【消息的传递可能会受到网络延迟、机器故障或其他因素的影响,从而导致消息的顺序或者到达时间发生变化。】

    #  加入了MQ,帮项目提升了些效果,但是管理MQ这些成本远超提升的这些效果,就不适合MQ了。
    #  分布式项目(你的应用需要在不同的组件之间进行异步通信),如果是单体项目就使用消息队列可能有点过度设计。

    #  总结:使用消息队列需要考虑系统的复杂性、异步通信的需求、高可用性、消息的可靠性和增量扩展等方面的因素,才能真正发挥消息队列的作用。







2. 为什么要学习RabbitMQ

在电子商务平台中处理订单和库存管理。假设你正在开发一个电子商务网站,以下是一个使用 RabbitMQ 的场景:
  1. 用户下单购买商品后,系统需要及时更新库存信息并处理订单。
  2. 同时,系统还需要发送订单确认邮件给用户,以及触发其他后续操作(如支付处理、物流安排等)。








3. 常见MQ产品

  • ActiveMQ:基于JMS
  • RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
  • RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会
  • Kafka:分布式消息系统,高吞吐量

image








4. RabbitMQ架构

4.1 简单架构图




4.2 完整架构图

RabbitMQ中有多个Virtual Host。

image




4.3 交换机类型

在RabbitMQ中有五种交换机类型。

  • default
  • fanout Publish/Subscribe
  • direct Routing
  • topic Topics
  • headers



4.4 交换机操作界面

在RabbitMQ中可以实现数据隔离,因为有虚拟机的概念,我们可以在rabbitMQ中创建不同的虚拟机,给不同的用户使用。
比如公司有多个项目都要用到rabbitMQ,我们可以为不同的项目分配不同的虚拟机、每个虚拟机设置旗下的用户,登陆响应的用户才能操作相应的虚拟机。从而达到数据隔离的效果。

image



如何实现数据隔离:

  1. 先创建一个用户
    image

  2. 创建虚拟机
    image

  3. 将虚拟机分配给用户
    image
    image
    image
    image
    这样一来,test001用户就只能操作 for-test001 虚拟机了。从而达到数据隔离的效果。
    image








5. RabbitMQ的通讯方式

如图所示:rabbitMQ的通讯方式有好几种,这里只介绍最常用的几种。

  1. publish/subscribe————>fanout交换机
    fanout交换机会将接收到的消息路由到每一个与其绑定的queue,所以也叫广播模式。
    image



  1. routing————>direct交换机
    direct交换机会将接收到的消息根据路由规则路由到指定的queue,因此也成为定向路由。
  • 每一个queue都与exchange设置一个bindingkey
  • 发布者发布消息时,指定消息的routingkey
  • exchange将消息路由到bingdingkey与消息routingkey一致的队列
    image



  1. topics————>topic交换机
    topic交换机也是基于routingkey做消息路由,但是routingkey通常是多个单词的组合,并且以,分割。
  • 每一个queue都与exchange设置一个bindingkey
  • 发布者发布消息时,指定消息的routingkey
  • exchange将消息路由到bingdingkey与消息routingkey一致的队列
  • *代表1个占位,#代表多个占位
    image






7. SpringBoot整合RabbitMQ[基于配置类]

整合各种模式可以参考:https://juejin.cn/post/6976033887449251876

7.1 导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>



7.2 编写配置文件

spring:
  rabbitmq:
    host: 47.114.216.28
    port: 5672
    username: admin
    password: admin
    virtual-host: test



7.3 创建配置类 —— 声明exchange,queue 并将其绑定

rabbitMQ的配置类可以声明在生产者端,也可以声明在消费者端 都可以。 但是通常在消费端

  • 注意:
    在声明交换机、队列时,可以用new方式创建,也可以通过构建者方式创建
    image

示例:创建topic交换机为例

@Configuration
public class RabbitMQConfig {

    //1. 创建exchange           以topic通讯方式为例,因为其最灵活
    @Bean
    public TopicExchange getTopicExchange(){
        // 参数1(String name):交换机名称    
        // 参数2(boolean durable):交换机是否持久化。如果为true,则RabbitMQ服务器在重启后仍然存在。如果为false,则在RabbitMQ服务器重启后将被删除。    
        // 参数3(boolean autoDelete):交换机是否自动删除。如果为true,表示当该交换机不再被任何队列或者交换机所绑定时,会自动被删除
        // 参数4(Map<String, Object> arg):声明交换机的参数
        return new TopicExchange("boot-topic-exchange",true,false);
    }

    

    //2. 创建queue
    @Bean
    public Queue getQueue(){
        //参数1:queue - 指定队列的名称                 【若该队列不存在,则自动创建】
        //参数2:durable - 当前队列是否需要持久化        【持久化:将队列接收到的消息持久化到硬盘,若队列中的消息还没被消费,就算RabbitMQ重启了该消息也不会丢失】
        //参数3:exclusive - 是否独占                   【当前队列只允许一个消费者连接可用,其他消费者再来连接时不能再用,当连接对象Connection被close()之后,当前队列会自动删除】
        //参数4:autoDelete - 是否自动删除              【表示当所有的消费者连接关闭后,是否自动删除该队列】
        //参数5:arguments - 声明当前队列的其他信息      【是一个键值对参数,表示队列的其他属性。可以设置各种参数,如消息过期时间、最大队列长度等】
        return new Queue("boot-queue",true,false,false,null);
    }

    //3. 创建Binding
    @Bean
    public Binding getBinding(TopicExchange topicExchange,Queue queue){
        // .bind()  绑定哪个队列  .to()  到哪个交换机  .with()  指定routingKey
        return BindingBuilder.bind(getQueue()).to(getTopicExchange()).with("*.red.*");
    }

}


//  上述写法可以写成下面这种=================================================================================


@Configuration
public class RabbitMQConfig {
    
    public static final String TOPIC_NAME = "topicExchange";
    public static final String QUEUE_NAME = "topicQueue";


    //1. 创建exchange           以topic通讯方式为例,因为其最灵活
    @Bean("exchange")
    public Exchange getTopicExchange(){
        return ExchangeBuilder.topicExchange(TOPIC_NAME).durable(true).build();     // 它是这种构建者的方式构建来指定  
    }

    

    //2. 创建queue
    @Bean("queue")
    public Queue getQueue(){
        return QueueBuilder.durable(QUEUE_NAME).build();   // 他也是这种构建者
    }

    //3. 创建Binding
    @Bean
    public Binding getBinding(@Qualifier("exchange")Exchange exchange,@Qualifier("queue")Queue queue){   // 这个配置类可能又很多交换机和配置类,所以一般都会用  @Bean注解指定它的名称。而这里在绑定的时候,也要区分不同的交换机与队列.
        // .bind()  绑定哪个队列             .to()  到哪个交换机               .with()  指定routingKey       .noargs()  不需要指定参数-如果不写这个也代表不需要指定参数
        return BindingBuilder.bind(queue).to(topicExchange).with("*.red.*").noargs();
    }

}



7.4 发布者发布消息到rabbitMQ

注意:在发送消息时,我们可以发送任意类型的消息,比如字符串、对象等,我们写消费者接收时,他怎么知道我们发的什么类型呢?不用担心,你发的时候发的是什么,接收的时候就用什么来接收就行,mq底层有个消息转换器,会自动进行转换。

现象:mq那个底层的消息转换器,对于对象的转换不友好,我们在mq的控制台可以看到消息的内容,但是呢,是转吗之后的,看着不友好。如果想看着友好些,就需要改一下mq底层用的消息转换器。
image

———————————————————————————————————————————————————————————

@SpringBootTest
class SpringbootRabbitmqApplicationTests {


    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void contextLoads() throws IOException {
        // 参数1(String exchange):指定交换机    
        // 参数2(String routingKey):指定routingKey    
        // 参数3(Object message):指定发送的数据内容
        // 参数4(MessagePostProcessor m):用于修改消息的属性
        // void convertAndSend(String exchange, String routingKey, Object message, MessagePostProcessor m)

        // 如果使用 convertAndSend 方法发送消息时指定的交换机不存在,或者消息发送失败,会抛出 AmqpException 异常。因此,建议在使用 convertAndSend 方法发送消息时进行异常处理。
        rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!");
    }

}



7.5 消费者监听rabbitMQ接收消息

// 编写监听类
@Component
public class Consumer {
	
// @RabbitListener 注解有以下常用属性:
    id:指定监听器的 ID,可以用于唯一标识一个监听器。
    containerFactory:指定 RabbitListenerContainerFactory 的名称,用于创建消息监听容器。
    queues:指定监听的队列名称或队列对象,可以是一个字符串数组或 Queue 对象数组。
    exclusive:指定是否独占队列,默认为 false。
    priority:指定消费者的优先级,优先级越高的消费者优先处理消息。
    group:指定消费者组的名称,用于多个消费者共同消费同一个队列时进行分组。
    concurrency:指定消费者的并发数,默认为 1,即每次只能处理一个消息。
    autoStartup:指定监听器是否自动启动,默认为 true。
    ackMode:指定消息的确认模式,可以是 AcknowledgeMode.AUTO、AcknowledgeMode.MANUAL 或 AcknowledgeMode.NONE。

// 使用 @RabbitListener 注解标记的方法可以接受多种类型的参数,包括:
    byte[] 或 String:用于接收消息的原始字节数组或字符串形式。
    org.springframework.messaging.Message或其子类:表示经过消息转换后的消息对象,其中包含了消息的 payload 和 headers。
    org.springframework.amqp.core.Message或其子类:表示接收到的原始消息对象。
    自定义 POJO 类型:可以根据需要自定义一个 POJO 类型,用于接收消息的内容。
    Channel: 它是 RabbitMQ 客户端与服务器之间的通信信道

    @RabbitListener(queues = "boot-queue")       // 指定要监听的队列
    // 将来来消息了,就会被这个Message对象接收。这里为什么用message呢,其实也可以用跟发送方发送时的消息类型一样,如果发送的时候传的是个XX自定义对象,你也可以用那个自定义对象来接收,message是顶层的接口而已,可以通过它拿到传过来的数据。
    public void getMessage(Message message) throws IOException {  
        System.out.println("接收到消息:" + message);
    }
}









8. 思考几个问题

思考几个问题?

  1. 生产者在发布消息时,由于网络波动,没有连接上mq,消息会丢失吗? 重试连接机制
    会。可利用连接失败后的重试机制解决。

image



  1. 生产者在发布消息到RabbitMQ的交换机时,由于网络问题,导致没有真发送成功到交换机,消息会丢失吗? confirm机制
    会,生产者执行了发布消息的方法,就会认为已经发布过去了。可利用 Confirm(确认)机制 实现或利用其提供的 事务操作机制【影响效率,这里不介绍它】。   PS:Confirm机制是保证了消息发送到Exchange上。而消费者监听的不是Exchange,而是队列。


  1. 生产者成功发布消息到交换机了,但是交换机分发消息到队列的时候出现了问题,导致没有真分发成功,消息会丢失吗? return机制
    会,消费者是与队列交互的,如果消息没有分发到队列,队列就没有消息。可利用Return机制实现。   PS:Return机制是保证Exchange的消息分发到队列。


  1. 如果消息已经到达了RabbitMQ,还没发送给消费者时,RabbitMQ宕机了,消息会丢失吗? 持久化机制
    会,RabbitMQ的队列提供了持久化机制,若消息到了RabbitMQ已经到了队列那里了,就能持久化。当RabbitMQ重连的时候消息就能发送给消费者了。


  1. 消费者在消费消息的时候,还没消费完,此时消费者宕机了,消息会丢失吗? 手动ack
    会,RabbitMQ提供了手动ACK。当成功消费完消息的时候再手动ACK告诉生产者我消费完了。








9. confirm机制与return机制

9.2 开启confirm机制与return机制

9.2.1 SpringBoot方式实现RabbitMQ的confirm与return机制
# 1. 配置文件开启confirm与return机制  通常是在生产方

spring:
  rabbitmq:
    host: 47.144.116.28
    port: 5672
    username: admin
    password: admin
    virtual-host: test

    publisher-confirm-type: simple  # 开启confirm机制
    publisher-returns: true    # 开启return机制

# 关于publisher-confirm-type的取值
NONE:        禁用发布确认模式,是默认值
CORRELATED:  发布消息成功到交换器后会触发回调方法(异步)
SIMPLE:      发布消息成功到交换器后会触发回调方法(同步)
// 2. 创建关于  SpringBoot 实现可靠性 的配置类[return与confirm的配置类]

@Component
public class PublisherConfirmAndReturnConfig implements RabbitTemplate.ConfirmCallback  ,  RabbitTemplate.ReturnCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct     // 表示在初始化PublisherConfirmAndReturnConfig对象时,会执行该方法。
    public void initMethod(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }

    // 基于confirm的回调   此方法用于监听消息是否发送到交换机
    // correlationData:对象内部只有一个 id 属性,用来表示当前消息的唯一性。
    // ack:消息投递到broker 的状态,true表示成功。
    // cause:表示投递失败的原因
    @Override    
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if(ack){
            System.out.println("消息已经送达到Exchange");
        }else{
            System.out.println("消息没有送达到Exchange");
        }
    }

    // 基于return的回调    此方法用于监听消息是否发送到队列   注意:消息没有送达队列时该方法才会执行
    // message:无法路由到目标队列的消息对象。
    // replyCode:返回码,指示无法路由到目标队列的原因。
    // replyText:返回信息,包含返回码的解释。
    // exchange:发送消息时指定的交换机名称。
    // routingKey:发送消息时指定的路由键。
    @Override  
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("消息没有送达到Queue");
    }
    
}
生产者发布消息

@Autowired
private RabbitTemplate rabbitTemplate;

@Test
void contextLoads() throws IOException {
    rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!");
}
消费者消费消息

@Component
public class Consumer {
	
	@RabbitListener(queues = "boot-queue")
	public void getMessage(Message message, Channel channel,String msg) throws IOException {
		System.out.println("接收到消息:" + message);
	}
	
}








10. 持久化

持久化可以提高 RabbitMQ 的可靠性,以防在异常情况(重启、关闭、宕机等)下的数据丢失。
持久化可分为以下几种情况:

  • 交换机的持久化
  • 队列的持久化
  • 消息的持久化

10.1 交换机的持久化

交换器的持久化是在声明交换器的时候,将 durable 属性设置为 true。如果交换器不设置持久化,那么在 RabbitMQ 服务重启之后,相关的交换机就会被删除。
交换机的持久化:是保证重启后交换机不丢失, 交换机并没有持久化消息的功能。

@Bean
public TopicExchange payTopicExchange(){
    // 参数1(String name):交换机名称    
    // 参数2(boolean durable):交换机是否持久化。如果为true,则RabbitMQ服务器在重启后仍然存在。如果为false,则在RabbitMQ服务器重启后将被删除。    
    // 参数3(boolean autoDelete):交换机是否自动删除。如果为true,表示当该交换机不再被任何队列或者交换机所绑定时,会自动被删除
    // 参数4(Map<String, Object> arg):声明交换机的参数
    return new TopicExchange(exchangeMame,true,false);
}




10.2 队列的持久化

队列的持久化也是在声明队列的时候,将durable参数设置为true。如果队列不设置持久化,那么 RabbitMQ服务重启之后,队列就会被删除,既然队列都不存在了,队列中的消息也会丢失。

 @Bean
public Queue dlQueue(){
    //参数1:queue - 指定队列的名称                 【若该队列不存在,则自动创建】
    //参数2:durable - 当前队列是否需要持久化        【持久化:将队列接收到的消息持久化到硬盘,若队列中的消息还没被消费,就算RabbitMQ重启了该消息也不会丢失】
    //参数3:exclusive - 是否独占                   【当前队列只允许一个消费者连接可用,其他消费者再来连接时不能再用,当连接对象Connection被close()之后,当前队列会自动删除】
    //参数4:autoDelete - 是否自动删除              【表示当所有的消费者连接关闭后,是否自动删除该队列】
    //参数5:arguments - 声明当前队列的其他信息      【是一个键值对参数,表示队列的其他属性。可以设置各种参数,如消息过期时间、最大队列长度等】
    return new Queue(dlQueue,true);
}




10.3 消息的持久化

image

队列的持久化能保证其本身不会因重启、关闭、宕机的情况而丢失,但是并不能保证其内部所存储的消息不会丢失。

要确保消息不会丢失,需要将消息设置为持久化。

在发送消息的时候,通过将BasicProperties中的属性deliveryMode(投递模式)设置为 2或者MessageDeliveryMode.PERSISTENT 即可实现消息的持久化。

    // springboot方式

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMessage(String message) {
        rabbitTemplate.convertAndSend("exchangeName", "routingKey", message, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                return message;
            }
        });
    }

可以将所有的消息都设置为持久化,但是这样会严重影响 RabbitMQ 的性能。写入磁盘的速度比写入内存的速度慢得不只一点点。
对于可靠性不是那么高的消息可以不采用持久化处理,以提高整体的吞吐量。在选择是否要将消息持久化时,需要在可靠性和吞吐量之间做权衡。
队列的持久化,消息本身的持久化 ———————————————— 才是保证消息在MQ中不丢失的2种方式


一般的系统也用不到对消息进行持久化。不过交换机和队列的持久化还是要支持的。

上述几种方式我们只是保证了消息发送到交换机、队列时不会由于RabbitMQ的重启、关闭、宕机的情况而丢失消息。但如果消费者在消费的时候出现问题了呢?

对于消费者来说,如果在订阅消息的时候,将autoAck设置为了true(即:自动ack,一收到消息就确认,不管是否执行成功),消费者接收到相关消息后,还没有正式处理消息逻辑之前,就出现了异常挂掉了,但消息已经被自动确认了,这样也算数据丢失。
对此有如下几种方式解决:
    1. 可以用手动Ack,消费者成功消费后告诉mq我成功消费了。
    2. 将消息重试并设置死信队列








11. SpringBoot实现手动ACK

RabbitMQ的确认机制是自动确认的,消费者收到消息后立马确认。
image


自动确认:可能出现消费者最后没有成功消费信息的可能,所以我们一般需要手动确认(通过调用 channel.basicAck()),即在成功消费后再告诉MQ。


如果消费者在消费过程中,出现了异常,我们可以手动捕获异常后调用 basicNack或basicReject绝消息,让MQ重新发送。


关于basicNack或basicReject下方代码有说明,请注意查看。


如果出现代码异常,不管你是那种消息确认机制,只要是代码异常(你没有手动捕获),都会nack给生产者,生产者就会重新投递消息到消费者,直到消费者成功消费为止。就出现了一个问题,他这样一直不停的投递,容易出现内存溢出问题。 还有上述情况,调用了basicNack或basicReject且requeue设置的是true,这种还是会出现不断重复投递。怎么解决呢? 看第15个章节。


11.1 更改配置文件 通常是在消费方

在上述操作基础上更改

spring:
  rabbitmq:
    host: 47.174.116.28
    port: 5672
    username: admin
    password: admin
    virtual-host: test
    listener:
      simple:
        acknowledge-mode: manual     # auto:自动(默认)   manual:表示手动ACK   

11.2 更改消费者

开启手动ack后,如果消费了消息后,只要消费者不ack,则在MQ中会显示该消息未被消费。

@Component
public class Consumer {
	
	@RabbitListener(queues = "boot-queue")
	public void getMessage(Message message, Channel channel,String msg) throws IOException {

                (确认消息)
                // deliveryTag :消息的编号     【需要通过: message.getMessageProperties().getDeliveryTag()  获取】
                // multiple:是否批量进行签收。
                      // true: 批量签收所有消息。 
                      // false:表示不批量签收,签收该消息编号的消息
                void basicAck(long deliveryTag, boolean multiple) throws IOException; 用于确认当前消息


                (★★★★★★★★★★★★★★★★★★★★★★★★★★★拒绝消息★★★★★★★★★★★★★★★★★★★★★★★★★★★)
                // deliveryTag:消息的编号      
                // multiple:是否批量拒绝。
                      // true: 批量拒绝所有消息。 
                      // false:表示不批量拒绝,拒绝该消息编号的消息
                // requeue:
                      // true:会重新将这条消息放回队列并重新发送该消息
                      // false:会立即把消息从队列中移除。如果你为该交换机设置了死信交换机,则该消息会被变成死信消息,就能实现转发到死信队列。
                      // 如:channel.BasicNack(3, true, false);  第一个参数编号中如果输入3,则消息DeliveryTag小于等于3的,这个Channel的,都会被拒收 
                void basicNack(long deliveryTag, boolean multiple, boolean requeue); 用于拒绝当前消息   // ############【可以一次性拒绝多个】############


                (★★★★★★★★★★★★★★★★★★★★★★★★★★★拒绝消息★★★★★★★★★★★★★★★★★★★★★★★★★★★)
                // deliveryTag: 消息的编号      
                // requeue:
                      // true:会重新将这条消息放回队列并重新发送该消息
                      // false:会立即把消息从队列中移除。如果你为该交换机设置了死信交换机,则该消息会被变成死信消息,就能实现转发到死信队列。
                void basicReject(long deliveryTag,  boolean requeue); 用于拒绝当前消息       // ############【只能一次性拒绝1个】############


                // basicNack与basicReject区别:
                      // basicReject方法一次只能拒绝一条消息,如果想要批量拒绝消息,则可以使用 basicNack 这个方法。
 
                // ★★★★★★★★★尤其注意:如果消息被你拒绝,且你设置了 requeue 为true,则消息会回到队列重新发送给其他消费者(多个的时候,一个的时候就只发给原先发送的那个消费者),若再次发送消息,代码逻辑还是拒绝,则消息会不断一直不停的重新发送。形成了‘死锁’。★★★★★★★
                
		System.out.println("接收到消息:" + message);
		channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
	}
	
}








12. RabbitMQ 的重试机制

消费端在处理消息过程中可能会报错,亦或者你拒绝了消息,导致消息不断重试投递,此时该如何重新处理消息呢?解决方案有以下几种:

  1. 开启RabbitMQ的重试机制(默认不开启),让该消费者重试消费消息,有可能能解决(如果是因为突然的网络问题这种有可能重试能解决,如果就是代码逻辑有问题,重试肯定还是发报错)。
    mq提供的消费者失败重试机制,不是让消费者失败后,又由生产者投递到消费者消费直到消费成功为止,他是在消费者出现异常时,利用消费者本地重试,而不再是直接又nack给生产者,生产者又无限投递给消费者,陷入循环。重试并不是RabbitMQ重新发送了消息,仅仅是消费者内部进行的重试。
    注意:如果你开启了重试机制,重试达到你的最大次数后还是没成功,消息就会丢失,他会报一个异常,RejectAndDontRequeueRecoverer,就是消息不回队列的意思,怎么解决呢?见下面。

  1. 开启RabbitMQ的重试机制后,使用redis记录重试次数,达到指定次数后,拒绝该消息。

  1. 在有可能有异常的地方直接try-cathch捕获了,或者把整个方法的逻辑都try-cathch捕获了,捕获异常后,直接拒绝消息,且在调用拒绝方法时,方法参数中的requeue设置成false(此时拒绝的话就会变成死信,若配置了死信队列,就会转发到死信队列)。


如果你选择第一种方案,使用mq提供的重试机制。

image

# retry功能的开启:
spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者进行重试次数限制    默认情况下是 false   
          max-attempts: 5 # 最大重试次数
          max-interval: 10000   # 重试最大间隔时间
          initial-interval: 3000 # 重试初始间隔时间

# ☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆:重试并不是RabbitMQ重新发送消息给队列,队列再发送给消费者。  而是队列里面的消息重新发送给消费者。


情况:

    若你开启了重试机制,并指定了重试次数,在重试完次数后,消费者还没成功消费该消息(ack该消息),那么该消息就会丢失。 需要有MessageRecoverer接口来处理那个消息的最终处理方案。

image


以第三种实现为例:
image









13. 如何对消息设置过期时间

在RabbitMQ中分别可以对 消息,队列 设置过期时间【这里的过期时间指:消息等待消费者消费的等待时间,而不是包含被消费者消费过程中所消耗的时间】

注意:消息到期后(如果是通过队列设置过期时间的消息到期,就会变成“死信”,而如果是消息本身设置的过期,到期后不会变成‘死信’)。

注:如果队列设置了死信队列,那么这条“死信”消息就会被转发到死信队列上,该消息就可以被正常消费。

注:若同时设置过期时间,谁先过期,先采用谁的。

住:若是通过队列设置的过期时间,则到期后,会立马删除,若有死信的话,会变成死信。
  若是通过消息属性设置的过期时间,到期后,可能不会立马删除,他底层实现是在消息即将投递到消费者之前判定的是否过期的,过期才删除


13.1 通过队列属性设置队列中消息过期时间

    @Bean
    public Queue payQueue(){
        Map<String,Object> params = new HashMap<>();
        //设置队列的过期时间
        params.put("x-message-ttl",10000);
        //也可以使用下面这种写法
        //QueueBuilder.durable("ttl-quequ").ttl(10000).build();
        return QueueBuilder.durable("ttl-quequ").withArguments(params).build();    // 构建者那种方式
    }

    // 不采用上述那种构建者那种方式
    @Bean
    public Queue getQueue(){
        Map<String,Object> params = new HashMap<>();
        //设置队列的过期时间
        params.put("x-message-ttl",10000);
        // 参数1:队列名称    参数2:是否持久化    参数3:是否独占    参数4:是否自动删除    参数5:指定当前队列的其他信息
        return new Queue("boot-queue",true,false,false,params);
    }

   
    // 此时,若该队列收到消息后,10秒内没有被消费者消费,则该消息就会从队列中被删除消失。

13.2 通过消息本身进行单独设置

    // 在发送消息的时候设置过期时间
    MessagePostProcessor messagePostProcessor = new MessagePostProcessor(){
        @Overeide
        public Message postProcessMessage(Message message) throws AmqpException{
            message.getMessageProperties().setExpiration("30000");
            return message;
        }
    };

    rabbitTemplate.convertAndSend("exchangeName","routingKey","消息内容",messagePostProcessor); // 指定过期时间,需要MessagePostProcessor对象

    // 【如果为消息本身设置了过期时间,当消息过期时。需要注意的是,RabbitMQ 并不会在每个消息的 TTL 到期时立即将其删除,而是在检查队列中的消息时,将所有已过期的消息标记为过期状态,并从队列中删除它们。】








14. 如何设置队列最长长度

    // 设置了最大长度后,当超出的消息,就会被丢失,如果设置了死心队列,则多的消息会被转发到死心队列。如最大长度为5,发了11条信息,则后6条会被转到死心队列去。
    @Bean
    public Queue getQueue(){
        Map<String,Object> params = new HashMap<>();
        // 设置队列的过期时间
        params.put("x-max-length",5);
        // 参数1:队列名称    参数2:是否持久化    参数3:是否独占    参数4:是否自动删除    参数5:指定当前队列的其他信息
        return new Queue("boot-queue",true,false,false,params);
    }








15. 什么是死信交换机与死信队列

出处:https://juejin.cn/post/6976778266472366087
ps:鉴于创建死信交换机比较繁琐,有大佬开发了死信交换机插件,只需要我们的rabbitmq安装该插件,使用起来很方便。地址:https://www.bilibili.com/video/BV1S142197x7?spm_id_from=333.788.player.switch&vd_source=61b6fb4e547748656e36b17ee95125fb&p=112
DLX ,全称为 Dead-Letter-Exchange ,可以称之为死信交换机。

它其实也是一个正常的交换机,和一般交换机没什么区别,它能在任何的队列上被指定。实现也很简单,实际上就是设置某个队列的属性。

当这个队列中存在死信消息时 RabbitMQ 就会自动地将这个消息新发布到设置的 DLX(死信交换机) 上去,进而被路由到另一个队列,即死信队列。我们可以监听这个死信交换机的死信队列中的消息、以进行相应的处理。

当消息在一个队列中变成死信之后,如果你对该队列配置了死信队列,那么它能被重新发送到另一个交换机中,这个交换器就是 DLX(死信交换机),而于 DLX 绑定的队列就称之为死信队列。

那种消息会变成死信?

  1. 消息过期,消息在的存活时间超过所设置的 TTL 时间。
  2. 消息被拒绝。调用了 channel.basicNack 或 channel.basicReject方法,井且设置 requeue 参数为false。
  3. 队列的接收消息数长度达到最大长度。

基于消息过期配置死信队列案例:

mq:
  queueBinding:
    queue: prod_queue_pay
    dlQueue: dl-queue
    exchange:
      name: exchang_prod_pay
      dlTopicExchange: dl-topic-exchange
    key: prod_pay
    dlRoutingKey: dl-routing-key

    //==============创建死信交换机并于死信队列进行绑定====================

    @Value("${mq.queueBinding.exchange.dlTopicExchange}")
    private String dlTopicExchange;
    
    @Value("${mq.queueBinding.dlRoutingKey}")
    private String dlRoutingKey;
    
    @Value("${mq.queueBinding.dlQueue}")
    private String dlQueue;
    
    //创建死信交换机  【可以是任意类型的交换机,这里采用的是topic类型的】
    @Bean
    public TopicExchange dlTopicExchange(){
        return new TopicExchange(dlTopicExchange,true,false);
    }
    
    //创建死信队列
    @Bean
    public Queue dlQueue(){
        return new Queue(dlQueue,true);
    }
    
    //死信队列与死信交换机进行绑定
    @Bean
    public Binding BindingErrorQueueAndExchange(Queue dlQueue, TopicExchange dlTopicExchange){
        return BindingBuilder.bind(dlQueue).to(dlTopicExchange).with(dlRoutingKey);
    }
    //==============创建要执行我们义务的交换机与队列====================
    //==============我们要基于队列设置过期时间=========================

    @Value("${mq.queueBinding.queue}")
    private String queueName;
    
    @Value("${mq.queueBinding.exchange.name}")
    private String exchangeName;
    
    @Value("${mq.queueBinding.key}")
    private String key;
    
    private final String dle = "x-dead-letter-exchange";    // 必须叫这个名称
    private final String dlk = "x-dead-letter-routing-key";   // 必须叫这个名称
    private final String ttl = "x-message-ttl";
    /**
     * 业务队列
     * @return
     */
    @Bean
    public Queue payQueue(){
        Map<String,Object> params = new HashMap<>();
        //设置队列的过期时间
        //队列中所有消息都有相同的过期时间
        params.put(ttl,10000);

        //==============================================================声明当前队列绑定的死信交换机============================================================================================
        params.put(dle,dlTopicExchange);
        //声明当前队列的死信路由键 如果没有指定,则使用原队列的路由键。因为我们指定的死信交换机是topic,所以会有路由键。如果是finaot模式,就可不配路由键。
        params.put(dlk,dlRoutingKey);
        
        return QueueBuilder.durable(queueName).withArguments(params).build();
    }
    
    @Bean
    public TopicExchange payTopicExchange(){
        return new TopicExchange(exchangeName,true,false);
    }
    
    //队列与交换机进行绑定
    @Bean
    public Binding BindingPayQueueAndPayTopicExchange(Queue payQueue, TopicExchange payTopicExchange){
        return BindingBuilder.bind(payQueue).to(payTopicExchange).with(key);
    }
    
    // 生产者发送消息

    @Component
    @Slf4j
    public class RabbitSender {

        @Value("${mq.queueBinding.exchange.name}")
        private String exchangeName;

        @Value("${mq.queueBinding.key}")
        private String key;

        @Autowired
        private RabbitTemplate rabbitTemplate;

        public void send(String msg){
            log.info("RabbitSender.send() msg = {}",msg);
            // 将消息发送给业务交换机
            rabbitTemplate.convertAndSend(exchangeName,key,msg);
        }

    }

启动服务,可以看到同时创建了业务队列、业务交换机以及死信队列、死信交换机。而且可以看到业务队列上带了 DLX、DLK标签。



然后调用接口:http://localhost:8080/?msg=红红火火 ,消息会被发送到 prod_queue_pay这


如果 10s 内没有消费者消费这条消息,那么判定这条消息为过期消息。由于设置了 DLX ,过期时消息被丢给 dlxExchange 交换机中,根据所配置的dlRoutingKey 找到与 dlxExchange 匹配的队列 dlQueue后,消息被存储在 dlxQueue这个死信队列中。









16. 如何保证RabbitMQ不重复消费消息

前言:

    这玩意并不是都要解决,要看具体情况!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    比如:新增订单,你的数据库中本来对于订单编号就有唯一限制,又或者该操作并不是 非幂等性操作,就没必要解决



11.1 为什么要解决重复消费问题

幂等性操作:就是指比如删除操作,这类操作执行多少次都没影响。
非幂等性操作:添加操作,而且数据库还是自增的,这类操作执行多次和执行一次差别是很大的!
image

所以,针对非幂等性操作,一定要保证消息不被重复消费。




11.2 重复消费消息的原因

回想一下消息到消费者的流程: 生产者——————>RabbitMQ——————>消费者。 可以看出,可能出现以下两种情况:

  1. 生产消息重复
  2. 消费消息重复

总结:不管是情况1还是情况2,只要保证消费者在消费消息的时候,不重复消费即可,所以我们只需要在消费消息的时候做处理即可。




消费消息时重复消费
消费者消费成功后,再给MQ确认,也就是ack的时候出现了网络波动。MQ以为消费者宕机了,网络恢复重连后,它就又投递了一次,这时候消费者有可能又收到了上次的消息,导致它又去处理,就处理了2次。
现象示例:https://blog.csdn.net/chenping1993/article/details/114580954




解决思路:
方法一(去重表):添加一张去重表,让每次消费之前,在数据表中添加一条记录,该记录其中某些字段添加唯一属性,若发生重复,则数据添加不上,添加上了才让其进消费者方消费。


方法二(全局唯一ID):让每个消息携带一个全局的唯一ID,引入redis,在消息被消费之前,将消息的唯一ID放到redis中,并设置它的值。

如值为0:正在执行业务,值为1:执行业务成功。
注意:一个比较极端的情况,消费者设置redis值为0后,执行业务,出现死锁,一直执行下去。所以,我们可以为这个redis设置一个过期时间,比如10秒之后,这个redis就消失。


具体消费过程为:
    1. 消费者获取到消息后先根据id去查询redis/db是否存在该消息
    2. 如果不存在,则设置redis的值为0(执行业务中,并设置过期时间),消费完毕后设置redis值为1(执行业务成功,并设置过期时间),并ack告诉MQ。
    3. 如果存在,判断其状态,若为1则证明消息被消费过,直接ack,若为0不执行任何操作。
  // 生产者发布消息
  
  @Test
  void contextLoads() throws IOException {
      
      // 用于创建消息的唯一标识
      CorrelationData messageId = new CorrelationData(UUID.randomUUID().toString());

      // rabbitMQ的convertAndSend() 方法就有一个带消息唯一标识的重载
      rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!",messageId);
      System.in.read();
  }

  @Autowired
  private StringRedisTemplate redisTemplate;


  // 消费者监听消息
  @RabbitListener(queues = "boot-queue")
  public void getMessage(String msg, Channel channel, Message message) throws IOException {
      //0. 获取MessageId    是从消息头里面的 spring_returned_message_correlation 获得的
      String messageId = message.getMessageProperties().getHeader("spring_returned_message_correlation");

      //1. 设置key到Redis    setIfAbsent:就相当于  setnx【在指定的 key 不存在时,为 key 设置指定的值】
      if(redisTemplate.opsForValue().setIfAbsent(messageId,"0",10, TimeUnit.SECONDS)) {
          //2. 消费消息
          System.out.println("接收到消息:" + msg);

          //3. 设置key的value为1
          redisTemplate.opsForValue().set(messageId,"1",10,TimeUnit.SECONDS);

          //4.  手动ack
          channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
      }else {

          //5. 获取Redis中的value即可 如果是1,手动ack
          if("1".equalsIgnoreCase(redisTemplate.opsForValue().get(messageId))){
              channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
          }
      }
  }








17. 延迟队列

延迟队列:消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
https://blog.csdn.net/weixin_60257072/article/details/128218999
场景举例:
      下订单后,30分钟如果还没支付,则取消订单回滚库存。
      该场景当然也可以用定时器来解决,每隔几分钟查看对方支付没得。太不优雅,如果数量梁很大的时候不好用

注意:在RabbitMQ中,没有延迟队列的概念,但是我们可以利用ttl和死信队列达到延迟的效果。

实现原理:

  1. 生产者生产一个消息到队列1
  2. 队列1中的消息并没有消费者,等它消息过期,被转发到死信队列
  3. 消费者获取死信队列的信息进行消费








18. RabbitMQ 消息积压问题

消息积压:指的是在消息队列中积压了一定量的消息,但消费者无法及时消费的情况。也就是说消息消费速率小于消息生产速率。

危害:有可能导致消费端宕机。

如何解决:
上线前:
   通过压测,判断预估流量,若预计每秒产生3000条消息,而你的消费者每秒能消费500条消息。那么就需要事先多部署几台消费者。或者优化消费消息的过程。
已上线:

    做了什么活动,流量激增。

    方法1:扩容消费者,或者设置限流(在MQ的配置中配置"最大消费者数量"与"每次从队列中获取的消息数量")

    方法2:给消息设置时间,超时就丢弃,保证不宕机。

    方法3:发送者发送流量太大上线更多的消费者,紧急上线专门用于记录消息的队列,将消息先批量取出来,记录数据库,离线慢慢处理。








19. RabbitMQ 消息顺序性

前言:在RabbitMQ中,队列中的消息是有序的,先进先出。

    但是,消费者的消费效率可能是不同的。
        举例:
            生产者按顺序发送了3条消息给一个队列,该队列有3个消费者,恰好3个消费者依次获取到了3条消息,每个消费者1条消息。
            消息A、B、C按顺序进入队列,消费者A1拿到消息A、消费者B1拿到消息B, 结果消费者B执行速度快,就跑完了,又或者消费者A1挂了,都会导致消息顺序不一致!!!

    解决方案:
        就是一个队列只有一个消费者!!!
        即:将要保证顺序性的消息,放到同一个队列,该队列的消息被同一个消费者消费。
        消息A、B、C按顺序进入队列,消费者从队列中依次获取消息消费,这样就保证消息顺序一致!!!
posted @ 2021-04-08 01:03  &emsp;不将就鸭  阅读(2118)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3