RabbitMQ

RabbitMQ

  • 1. 为什么要用消息队列
    • 1.1 通过异步处理提高系统性能(减少响应所需时间)
    • 1.2 削峰
    • 1.3 解耦
  • 2. RabbitMQ 核心概念
  • 3 .六种工作模式
    • 3.1 简单模式
    • 3.2 work工作模式(资源的竞争)
    • 3.3 publish/subscribe发布订阅(共享资源)
    • 3.4 路由模式
    • 3.5 topic 主题模式(路由模式的一种)
  • 4.消息基于什么传输?
  • 5. 保证消息的可靠性
    • 5.1 rabbitmq弄丢了数据(交换机持久化,队列持久化,消息持久化)
    • 5.2 如何确保消息正确地发送至RabbitMQ?
      • 5.2.1 如何实现Confirm确认消息
    • 5.3 如何确保消息接收方消费了消息?
    • 5.4 什么情况下会产生消息丢失的现象
  • 6. 镜像集群模式
  • 8. 死信队列(DLX,dead-letter-exchange)
    • 8.1 消息变成死信有以下几种情况
    • 8.2 死信处理过程
    • 8.3 死信消息的变化
  • 9. 延迟队列
  • 10. Fair dispath 公平分发
  • 面试题
    • 如何保证 RabbitMQ 消息的顺序性?
      • 1. 单个消费者实例
      • 2. 多个消费者实例(todo)
    • 如何避免消息重复投递或重复消费?
    • 防止mq消息堆积
      • 消息堆积主要原因:
      • 解决:
    • 如何解决消息队列的延时以及过期失效问题?
    • RabbitTemplate API

1. 为什么要用消息队列

1.1 通过异步处理提高系统性能(减少响应所需时间)

应用场景:用户注册后,需要发注册短信。
1.串行方式(用户请求用户系统进行注册,注册完成后,调用message系统进行发送短信提醒,等message系统处理完以后才给用户注册结果)
2.并行方式(用户请求用户系统进行注册,注册完成后,给message系统发送消息,异步发送短信提醒,返回注册结果)
· 提升用户体验

1.2 削峰

应用场景:秒杀活动中
RabbitMQ_第1张图片
1.服务器收到请求后,首先写入消息队列,加入消息队列长度超过最大值,则直接返回售罄等提示
2.秒杀业务根据消息队列中的请求信息,再做后续处理.
· 降低服务器压力,保证后续正常工作

1.3 解耦

应用场景:用户下单后,订单系统需要通知库存系统,如果库存系统挂掉会影响用户下单。
RabbitMQ_第2张图片
订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
库存系统:订阅下单的消息,获取下单消息,进行库操作。
就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失。

2. RabbitMQ 核心概念

Message :消息,由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、 priority(相对于其他消息的优先权)、 delivery-mode(指出该消息可能需要持久性存储)等。

Routing Key : 路由关键字,exchange根据这个关键字进行消息投递。

Exchange :交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。Exchange有4种类型: direct(默认), fanout, topic, 和headers,不同类型的Exchange转发消息的策略有所区别。 Exchange 会根据 routing key 和 Exchange Type(交换器类型) 以及 Binding key 的匹配情况来决定把消息路由到哪个 Queue。

Queue : 消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。

Binding : 绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和Queue的绑定可以是多对多的关系。

Producer:消息生产者,就是投递消息的程序.

Consumer:消息消费者,就是接受消息的程序.

Channel:信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接, AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。

3 .六种工作模式

3.1 简单模式

RabbitMQ_第3张图片
消息的消费者监听消息队列,如果队列中有消息,就消费掉,消息被拿走后,自动从队列中删除(隐患:消息可能没有被消费者正确处理,已经从队列中消失了,造成消息的丢失)
应用场景:聊天(中间有一个过度的服务器;p端,c端)

3.2 work工作模式(资源的竞争)

RabbitMQ_第4张图片
消息产生者将消息放入队列,消费者可以有多个,消费者1,消费者2同时监听同一个队列。
消息被谁消费?
C1 C2共同争抢当前的消息队列内容,谁先拿到谁负责消费消息(隐患:高并发情况下,默认会产生某一个消息被多个消费者共同使用,可以设置一个开关(syncronize,与同步锁的性能不一样) 保证一条消息只能被一个消费者使用)
应用场景:红包;大项目中的资源调度(任务分配系统不需知道哪一个任务执行系统在空闲,直接将任务扔到消息队列中,空闲的系统自动争抢)

3.3 publish/subscribe发布订阅(共享资源)

RabbitMQ_第5张图片
消息产生者将消息放入交换机,交换机发布订阅把消息发送到所有消息队列中,对应消息队列的消费者拿到消息进行消费。
相关场景:邮件群发,群聊天,广播(广告)

3.4 路由模式

RabbitMQ_第6张图片
消息根据路由key转发到指定队列

3.5 topic 主题模式(路由模式的一种)

RabbitMQ_第7张图片
*代表多个单词,#代表一个单词
消息产生者产生消息,把消息交给交换机,交换机根据key的规则模糊匹配到对应的队列,由队列的监听消费者接收消息消费

4.消息基于什么传输?

由于TCP连接的创建和销毁开销较大。RabbitMQ使用信道的方式来传输数据。信道是建立在真实的TCP连接内的虚拟连接,且每条TCP连接上的信道数量没有限制。就是说 RabbitMQ 在一条 TCP 链接上建立成百上千个信道来达到多个线程处理,这个 TCP 被多个线程共享,每个信道在 RabbitMQ 都有唯一的 ID,保证了信道私有性,每个信道对应一个线程使用。

5. 保证消息的可靠性

5.1 rabbitmq弄丢了数据(交换机持久化,队列持久化,消息持久化)

rabbitmq持久化分为三个部分: 交换器的持久化、队列的持久化和消息的持久化

交换器的持久化
交换器的持久化是通过声明队列时,将durable参数设置为true实现的。如果交换器不设置持久化,那么rabbitmq服务重启之后,相关的交换器元数据将会丢失,不过消息不会丢失,只是不能将消息发送到这个交换器中了,建议将交换器设置为持久化
hannel.ExchangeDeclare(ExchangeName, “direct”, durable: true, autoDelete: false, arguments: null);//声明消息队列,且为可持久化的

队列的持久化
队列的持久化是通过声明队列时,将durable参数设置为true实现的。如果队列不设置持久化,那么rabbitmq服务重启之后,相关的队列元数据将会丢失,而消息是存储在队列中的,所以队列中的消息也会被丢失。
channel.QueueDeclare(QueueName, durable: true, exclusive: false, autoDelete: false, arguments: null);//声明消息队列,且为可持久化的

消息的持久化
队列的持久化只能保证其队列本身的元数据不会被丢失,但是不能保证消息不会被丢失。所以消息本身也需要被持久化,可以在投递消息前设置AMQP.BasicProperties的属性deliveryMode为2即可:
channel.basicPublish(“”, queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
而且持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,rabbitmq挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。

哪怕是你给rabbitmq开启了持久化机制,也有一种可能,就是这个消息写到了rabbitmq中,但是还没来得及持久化到磁盘上,结果不巧,此时rabbitmq挂了,就会导致内存里的一点点数据会丢失。

5.2 如何确保消息正确地发送至RabbitMQ?

生产者将数据发送到rabbitmq的时候,可能数据就在半路给搞丢了,因为网络啥的问题,都有可能。

两种方案,rabbitmq事务和confirm模式,由于rabbitmq事务机制耗性能,因此建议开启confirm模式。
注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错

开启confirm模式,在生产者那里设置开启confirm模式之后,你每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq中,rabbitmq会给你回传一个ack消息,告诉你说这个消息ok了。如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息id的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。
异步确认模式需要在添加一个ConfirmListener,并且使用一个sortset来维护一个没有被确认的消息。

5.2.1 如何实现Confirm确认消息

第一步,在channel上开启确认模式:channel.confirmSelect()
第二步,在channel上添加监听:addConfirmListener,监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、或记录日志等后续处理!

    //用于保存维护未被确认的消息,要有序set  这里就是要treeset
    final SortedSet<Long> confirmSet = new TreeSet<>();
     channel.addConfirmListener(new ConfirmListener() {
         @Override
         public void handleAck(long deliveryTag, boolean multiple) throws IOException {
             System.out.println(String.format("Broker已确认消息,标识:%d,多个消息:%b", deliveryTag, multiple));
             //处理发送成功的情况的方法
             // deliveryTag,发送成功的消息的标志,multiple是否批量发送的
             if (multiple){
                 //如果true表示批量执行了deliveryTag这个值以前(小于deliveryTag的)的所有消息,如果为false的话表示单条确认
                 //如果为true,表示确认了此条条以及前面的n条的消息,需要批量重Sort中删除
                 //表示清除deliveryTag这条消息以及前面的消息,因为这个集合是维护未被确认的消息,而这些消息被确认了,就要删除掉了。
                 confirmSet.headSet(deliveryTag + 1L).clear();
                 //下面也可以做一些业务逻辑
             }else {
                 //单条确认
                 confirmSet.remove(deliveryTag);
             }

             System.out.println("未确认消息:" + confirmSet);
         }

         @Override
         public void handleNack(long deliveryTag, boolean multiple) throws IOException {
             //这个是处理未发送成功的消息
             System.out.println("Broker未确认消息,标识:" + deliveryTag);
             if (multiple) {
                 // headSet表示后面参数之前的所有元素,全部删除
                 confirmSet.headSet(deliveryTag + 1L).clear();
             } else {
                 confirmSet.remove(deliveryTag);
             }

             //这里可以写一些补偿逻辑
         }
     });
     String message = "asyncConfirmTest";
     channel.confirmSelect();
     for (int i = 0;i<10;i++){
         //获取下一次发送的发送Id
         long nextPublishSeqNo = channel.getNextPublishSeqNo();
         //加到为确认队列中
         confirmSet.add(nextPublishSeqNo);
         channel.basicPublish("TransactionEx", "TransactionMsg", null, (message +"-"+ i).getBytes());

     }
     System.out.println("所有消息:"+confirmSet);

RabbitMQ_第8张图片
事务机制和cnofirm机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息rabbitmq接收了之后会异步回调你一个接口通知你这个消息接收到了。

5.3 如何确保消息接收方消费了消息?

rabbitmq如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,rabbitmq认为你都消费了,这数据就丢了。

这个时候得用rabbitmq提供的ack机制,简单来说,就是你关闭rabbitmq自动ack,可以通过一个api来调用就行,然后每次你自己代码里确保处理完的时候,再程序里ack一把。这样的话,如果你还没处理完,不就没有ack?那rabbitmq就认为你还没处理完,这个时候rabbitmq会把这个消费分配给别的consumer去处理,消息是不会丢的。

 if(msg.indexOf("3")>=0){
      //deliveryTag:该消息的index
      //multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息。
      //requeue:被拒绝的是否重新入队列
      channel.basicNack(envelope.getDeliveryTag(),false,true);
      System.out.println("消费者 nack:"+msg);
    }else{
      //
      channel.basicAck(envelope.getDeliveryTag(),false);
      System.out.println("消费者 ack:"+msg);
    }

5.4 什么情况下会产生消息丢失的现象

第一种:生产者弄丢了数据。生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能。
第二种:RabbitMQ 弄丢了数据。MQ还没有持久化自己挂了
第三种:消费端弄丢了数据。刚消费到,还没处理,结果进程挂了,比如重启了。
第四种:消息队列满了的情况下
解决方案
RabbitMQ_第9张图片

6. 镜像集群模式

RabbitMQ_第10张图片
创建的queue,无论元数据还是queue里的消息都会存在于多个实例上,然后每次你写消息到queue的时候,都会自动把消息到多个实例的queue里进行消息同步。
这样的话,好处在于,你任何一个机器宕机了,没事儿,别的机器都可以用。坏处在于,第一,这个性能开销也太大了吧,消息同步所有机器,导致网络带宽压力和消耗很重!第二,这么玩儿,就没有扩展性可言了,如果某个queue负载很重,你加机器,新增的机器也包含了这个queue的所有数据,并没有办法线性扩展你的queue
那么怎么开启这个镜像集群模式呢?我这里简单说一下,避免面试人家问你你不知道,其实很简单rabbitmq有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候可以要求数据同步到所有节点的,也可以要求就同步到指定数量的节点,然后你再次创建queue的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。

8. 死信队列(DLX,dead-letter-exchange)

当消息在一个队列中变成一个死信之后,如果配置了死信队列,它将被重新publish到死信交换机,死信交换机将死信投递到一个队列上,这个队列就是死信队列。

8.1 消息变成死信有以下几种情况

消息被拒绝(basic.reject / basic.nack),并且requeue = false
消息TTL过期
队列达到最大长度

8.2 死信处理过程

RabbitMQ_第11张图片

8.3 死信消息的变化

​ 如果队列配置了参数 x-dead-letter-routing-key 的话,“死信”的路由key将会被替换成该参数对应的值。如果没有设置,则保留该消息原有的路由key。

比如:
​ 如果原有消息的路由key是testA,被发送到业务Exchage中,然后被投递到业务队列QueueA中,如果该队列没有配置参数x-dead-letter-routing-key,则该消息成为死信后,将保留原有的路由keytestA,如果配置了该参数,并且值设置为testB,那么该消息成为死信后,路由key将会被替换为testB,然后被抛到死信交换机中。

9. 延迟队列

延迟队列指的是存储对应的延迟消息,消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。

RabbitMQ本身是没有延迟队列的,要实现延迟消息,一般有两种方式:

  1. 通过RabbitMQ本身队列的特性来实现,需要使用RabbitMQ的死信交换机(Exchange)和消息的存活时间TTL(Time To Live)。
  2. 在RabbitMQ 3.5.7及以上的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延迟队列功能。同时,插件依赖Erlang/OPT 18.0及以上。
    也就是说,AMQP 协议以及RabbitMQ本身没有直接支持延迟队列的功能,但是可以通过TTL和DLX模拟出延迟队列的功能。
    在这里插入图片描述

10. Fair dispath 公平分发

你可能也注意到了,分发机制不是那么优雅,默认状态下,RabbitMQ将第n个Message分发给第n个Consumer。n是取余后的,它不管Consumer是否还有unacked Message,只是按照这个默认的机制进行分发.
那么如果有个Consumer工作比较重,那么就会导致有的Consumer基本没事可做,有的Consumer却毫无休息的机会,那么,Rabbit是如何处理这种问题呢?
RabbitMQ_第12张图片

通过basic.qos方法设置prefetch_count=1,这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message,换句话说,在接收到该Consumer的ack前,它不会将新的Message分发给它
channel.basic_qos(prefetch_count=1)
注意,这种方法可能会导致queue满。当然,这种情况下你可能需要添加更多的Consumer,或者创建更多的virtualHost来细化你的设计。

面试题

如何保证 RabbitMQ 消息的顺序性?

1. 单个消费者实例

其实队列本身是有顺序的,所以对于需要保证顺序消费的业务,我们可以只部署一个消费者实例,然后设置 RabbitMQ 每次只推送一个消息,再开启手动 ack 即可,配置如下

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 #每次只推送一个消息
        acknowledge-mode: manual

这样 RabbitMQ 每次只会从队列推送一个消息过来,处理完成之后我们 ack 回应,再消费下一个,就能确保消息顺序性。

或者就一个 queue (消息队列)但是对应一个 consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的 worker 来处理。

2. 多个消费者实例(todo)

如何避免消息重复投递或重复消费?

先说为什么会重复消费:正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。

解决思路是:保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;保证消息等幂性;

在消息生产时,MQ内部针对每条生产者发送的消息生成一个inner-msg-id,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列;
在消息消费时,要求消息体中必须要有一个bizId(对于同一业务全局唯一,如支付ID、订单ID、帖子ID等)作为去重和幂等的依据,避免同一条消息被重复消费。

  1. 一般来说消息重复消费都是在短暂的一瞬间消费多次,我们可以使用 redis 将消费过的消息唯一标识存储起来,然后在消费端业务执行之前判断 redis 中是否已经存在这个标识。举个例子,订单使用优惠券后,要通知优惠券系统,增加使用流水。这里可以用订单号 + 优惠券 id 做唯一标识。业务开始先判断 redis 是否已经存在这个标识,如果已经存在代表处理过了。不存在就放进 redis 设置过期时间,执行业务。
  2. 数据库唯一键约束/数据库乐观锁思想 兜底

防止mq消息堆积

消息堆积主要原因:

  1. 消费者的速度大大慢于生产者的速度,速度不匹配从引起的堆积;
  2. 消费者故障期间消息的堆积。

解决:

  1. 增加消费者的处理能力(例如优化代码),或减少发布频率
  2. 可以通过设置并发消费提高消费的速率,从而减少消息堆积的问题 。默认情况下,rabbitmq消费者为单线程串行消费,这也是队列的特性。设置并发消费两个关键属性concurrentConsumers和prefetchCount
    concurrentConsumers设置的是对每个listener在初始化的时候设置的并发消费者的个数,prefetchCount是每次一次性从broker里面取的待消费的消息的个数,
    从源码中分析org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer:启动的时候会根据设置的concurrentConsumers创建N个BlockingQueueConsumer(N个消费者)。
    prefetchCount是BlockingQueueConsumer内部维护的一个阻塞队列LinkedBlockingQueue的大小,其作用就是如果某个消费者队列阻塞,就无法接收新的消息,该消息会发送到其它未阻塞的消费者
  3. 考虑使用队列最大长度限制.
  4. 给消息设置年龄,超时就丢弃

如何解决消息队列的延时以及过期失效问题?

RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上 12 点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。

RabbitTemplate API

你可能感兴趣的:(面试题-队列,java-rabbitmq,rabbitmq,java)