消息中间件的6大高频问题和答案

Posted Java小海.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了消息中间件的6大高频问题和答案相关的知识,希望对你有一定的参考价值。

Java面试必知必会系列》继续更新ing,今天给大家分享一些消息中间件相关的面试问题,希望你在面试中遇到后,可以回答的上来。

当然关于消息中间件的内容其实是非常非常多的,这里只是列举出来一些高频的问题,需要更多消息中间件的问题和答案。

消息中间件是基于队列与消息传递技术,在网络环境中为应用系统提供同步或异步、可靠的消息传输的支撑性软件系统。

这里给出6个非常高频问题,问题答案均来自网络。

  1. 消息队列的使用场景?
  2. 消息队列的优缺点?
  3. Kafka、ActiveMQ、RabbitMQ、RocketMQ之间的对比
  4. MQ如何保证消息的高可靠性?
  5. MQ如何保证消息不被重复消费?
  6. MQ如何保证消息的顺序性?

下面给出这些问题的答案,便于大家在面试的时候回答面试官,但一般面试官可能会结合你的项目问消息中间件的相关问题,一般项目里都会用到MQ,所以如果项目里用到了,一定要搞清楚,准备好面对面试官的”灵魂考问“!

1、消息队列的使用场景?

回答:消息队列主要有三大使用场景,分别是异步、流量削锋和应用解耦。另外还包含日志和消息通讯。

  • 异步处理 - 相比于传统的串行、并行方式,提高了系统吞吐量。
  • 应用解耦 - 系统间通过消息通信,不用关心其他系统的处理。
  • 流量削锋 - 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请 求。
  • 日志处理 - 解决大量日志传输。
  • 消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通 讯。比如实现点对点消息队列,或者聊天室等。

2、消息队列有什么优缺点?

回答:优点可以叙述第一题的作用,都是使用消息队列的好处。

缺点有以下几个:

系统可用性降低:系统引入的外部依赖越多,越容易挂掉,本来你就是A系统调用BCD三个系统的接口就好了,人ABCD四个系统好好的,没啥问题,你偏加个MQ进来,万一MQ挂了咋整?MQ挂了,整套系统崩溃了,你不就完了么。

  系统复杂性提高:硬生生加个MQ进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已

  一致性问题:A系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是BCD三个系统那里,BD两个系统写库成功了,结果C系统写库失败了,咋整?你这数据就不一致了。

3、Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点?

4、MQ如何保证消息的高可靠性?

【如何处理消息丢失的问题? 】

这里以RabbitMQ为例!

从三个方面来保证,分别是生产者、消息中间件和消费者

生产者丢了数据

复制代码

1

2

3

4

5

6

7

channel.confirmSelect();

…………

if (channel.waitForConfirms())

    System.out.println("发送消息成功");

else

    System.out.println("发送消息失败");

消息中间件丢了数据

消息服务器对应的队列、交换机等都持久化,保证数据的不丢失。

消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,自己就挂了,可能导致少量数据丢失,但是这个概率较小。

设置持久化有两个步骤:

  • 创建 queue 的时候将其设置为持久化这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
  • 第二个是发送消息的时候将消息的 deliveryMode 设置为 2就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。

必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。

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

所以,持久化可以跟生产者那边的 confirm 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 ack了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 ack,你也是可以自己重发的。

消费端弄丢了数据

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

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

5、MQ如何保证消息不被重复消费?

【或者说,如何保证消息消费的幂等性? 】

消息重复的原因有两个:1.生产时消息重复,2.消费时消息重复。

生产时消息重复

由于生产者发送消息给MQ,在MQ确认的时候出现了网络波动,生产者没有收到确认,实际上MQ已经接收到了消息。这时候生产者就会重新发送一遍这条消息。

生产者中如果消息未被确认,或确认失败,我们可以使用定时任务+(redis/db)来进行消息重试。

消费时消息重复

消费者消费成功后,再给MQ确认的时候出现了网络波动,MQ没有接收到确认,为了保证消息被消费,MQ就会继续给消费者投递之前的消息。这时候消费者就接收到了两条一样的消息。

由于重复消息是由于网络原因造成的,因此不可避免重复消息。但是我们需要保证消息的幂等性

如何保证消息幂等性

让每个消息携带一个全局的唯一ID,即可保证消息的幂等性,具体消费过程为:

  1. 消费者获取到消息后先根据id去查询redis/db是否存在该消息。
  2. 如果不存在,则正常消费,消费完毕后写入redis/db。
  3. 如果存在,则证明消息被消费过,直接丢弃。

6、MQ如何保证消息的顺序性?

解决方案

RabbitMQ

  • 拆分为多个queue,每个queue由一个consumer消费;
  • 或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理

Kafka

  • 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。
  • 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。

中间件面试专题:RabbitMQ高频面试问题




开篇介绍




Q:

RabbitMQ 的介绍、用途、好处?

RabbitMQ是一款开源的,Erlang编写的,基于AMQP协议的消息中间件。

作用:解耦异步削峰


优点解耦、异步、削峰;

缺点低了系统的稳定性:系统中使用了消息队列,如果消息队列挂了,那么系统也会挂。降低了系统可用性。


加入消息队列,要考虑很多方面的问题,比如:一致性问题 、如何保证消息不被重复消费 、 如何保证消息可靠性传输 等。因此考虑的因素有很多方面,复杂性增加。



Q:

RabbitMQ 包括哪些要素?

  • 生产者 :消息的创建者,发送到RabbitMQ

  • 消费者 :连接到RabbitMQ,订阅到队列上,消费消息,持续订阅(basicConsumer)和单条订阅(basicGet)

  • 消息 包含有效载荷和标签,有效载荷指要传输的数据,标签描述了有效载荷,并且RabbitMQ用它来决定谁获得消息,消费者只能拿到有效载荷,并不知道生产者是谁。



Q:

RabbitMQ 什么是信道?

道:是生产者、消费者与RabbitMQ通信的渠道,生产者publish或是消费者subscribe一个队列都是通过信道来通信的。信道是建立在TCP连接上的虚拟连接。就是说RabbitMQ在一条TCP上建立成百上千个信道来达到多个线程处理,这个TCP被多个线程共享,每个线程对应一个信道,信道在RabbitMQ都有一个唯一的ID,保证了信道私有性,对应上唯一的线程使用。


疑问:为什么不建立多个TCP连接?

原因是RabbitMQ需要保证性能,系统为每个线程开辟一个TCP是非常消耗性能的,美妙成百上千的建立销毁TCP会严重消耗系统性能;所以RabbitMQ选择建立多个信道(建立在TCP的虚拟连接)连接到RabbitMQ上



Q:

RabbitMQ概念里的channel、exchange 和 queue是逻辑概念,还是对应着进程实体?作用分别是什么?

queue 具有自己的 erlang 进程;

exchange 内部实现为保存 binding 关系的查找表;

channel 是实际进行路由工作的实体,负责按照 routing_key 将 message投递给queue。


由 AMQP 协议描述可知,channel 是真实TCP连接之上的 虚拟连接 , 所有AMQP 命令都是通过 channel 发送的,且每一个 channel 有 唯一的ID 。一个 channel 只能被单独一个操作系统线程使用,所以投递到特定的 channel 上的 message 是有顺序的。单一个操作系统线程上允许使用多个channel。



Q:

RabbitMQ消息是如何路由的?

消息路由必须有三部分:交换器路由绑定

生产者把消息发布到交换器上,绑定决定了消息如何从路由器路由到特定的队列;消息最终到达队列,并被消费者接收。

消息发布到交换器时,消息将拥有一个 路由键(routing key) , 在消息创建时设定。

通过队列路由键,可以把队列绑定到交换器上。

消息到达交换器后,RabbitMQ会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则)。如果能够匹配到队列,则消息会投递到相应队列中;如果不能匹配到任何队列,消息将进入"黑洞"。


常用的交换器主要分为以下三种:

  • direct :如果路由键完全匹配,消息就会被投递到相应的队列;每个AMQP的实现都必须有一个direct交换器,包含一个空白字符串名称的默认交换器。声明一个队列时,会自动绑定到默认交换器,并且以队列名称作为路由键:channel -> basic_public($msg, '', 'queue-name')

  • fanout : 如果交换器收到消息,将会广播到所有绑定的队列上;

  • topic :可以使来自不同源头的消息能够到达同一个队列。使用topic交换器时,可以使用通配符,比如:"*" 匹配特定位置的任意文本,"." 把路由键分为了几个标识符, "#" 匹配所有规则等。

特别注意:发往topic交换器的消息不能随意的设置选择键(routing_key),必须是有"."隔开的一系列的标识符组成。



Q

RabbitMQ消息确认过程?

      消费者收到的每一条消息都必须进行确认(自动确认和自行确认)

        消费者在声明队列时,可以置顶autoAck参数,当autoAck = false时,RabbitMQ会等待消费者显式发送回 ack 信号后才从内存(和磁盘,如果是持久化消息的话)中删除消息,否则RabbitMQ会在队列中消息被消费后立即删除它。

        采用消息确认机制后,只要使 autoAck = false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直持有消息直到消费者显式调用basicAck为止。

        当autoAck = false时,对于RabbitMQ服务器端而言,队列中的消息分成了两部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者ack信号的消息。如果服务器端一直没有收到消费者的ack信号,并且消费此消息的消费者已经断开连接,则服务器端会安排该消息 重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)。

        RabbitMQ不会为 ack消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开。这么设计的原因是RabbitMQ允许消费者消费一条消息的时间可以很久很久。



Q:

如何保证RabbitMQ不被重复消费?

正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认信息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除。

但是因为网络传输等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。


解决思路:

保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;

保证消息幂等性;

比如:在写入消息队列的数据做唯一标识,消费消息时,根据唯一标识判断该消息是否被消费过。




Q:

如何保证RabbitMQ消息的可靠传输?

消息不可靠的情况可能是消息丢失,劫持等原因;

丢失可能又分为:

  • 生产者丢失消息

  • 消息队列丢失消息

  • 消费者丢失消息


生产者丢失消息

从生产者弄丢数据来看,RabbitMQ提供了 transaction 机制 和 confirm 模式 来确保生产者不丢失消息;

  • transaction机制: 发送消息前,开启事务(channel.exSelect()),然后发送消息,如果发送过程中出现异常,事务就会回滚(channel.txRollback()),如果发送成功则提交事务(channel.txCommit())。

  • confirm模式:一般这种模式居多,一旦channel进入confirm模式,所有在该信道上发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列后;RabbitMQ就会发送一个ACK给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了。


如果RabbitMQ没能处理该消息,则会发送一个Nack消息回来,这样可以进行重试操作。


消息队列丢失消息

针对消息队列丢失数据的情况,一般是开启持久化磁盘的配置:

将队列的持久化标识 durable 设置为 true , 则代表是一个持久的队列,发送消息的时候讲 deliveryMode=2 这样设置以后,即使RabbitMQ挂了,重启后也能恢复数据。


消费者丢失消息

消费者丢失消息一般是因为采用了自动确认消息模式,改为手动确认消息即可。

消费者在收到消息之后,处理消息之前,会自动回复RabbitMQ已收到消息;如果这时候处理消息失败,就会丢失该消息;

解决方案:处理消息成功后,手动回复确认消息。



明天,会介绍RocketMQ面试题,长按二维码关注我吧~

祝大家都能拿到心仪的offer!



Java极客思维


以上是关于消息中间件的6大高频问题和答案的主要内容,如果未能解决你的问题,请参考以下文章

详解大数据中必不可少的消息中间件 kafka(3.x 新版本)

大数据消息中间件之Kafka02

支持百万级TPS,Kafka是怎么做到的?答案藏在这10张图里

查漏补缺:备战2021年java后端Kafka高频面试题(含答案解析)

大数据消息中间件之Kafka-01

面试官:消息中间件如何实现每秒几十万的高并发写入?石杉的架构笔记