开篇介绍
大家好,公众号【Java极客思维】近期会整理一些Java高频面试题分享给小伙伴,也希望看到的小伙伴在找工作过程中能够用得到!本章节主要针对Java一些消息中间件高频面试题进行分享。
通知:公众号【Java极客思维】正在送书福利活动,关注公众号并参加福利活动吧!只有参与了本次活动的小伙伴才能够参与年底的大福利,不要错过呀~
Q1:
RabbitMQ 的介绍、用途、好处?
RabbitMQ是一款开源的,Erlang编写的,基于AMQP协议的消息中间件。
作用:解耦 、 异步 、 削峰 。
优点:解耦、异步、削峰;
缺点:降低了系统的稳定性:系统中使用了消息队列,如果消息队列挂了,那么系统也会挂。降低了系统可用性。
加入消息队列,要考虑很多方面的问题,比如:一致性问题 、如何保证消息不被重复消费 、 如何保证消息可靠性传输 等。因此考虑的因素有很多方面,复杂性增加。
Q2:
RabbitMQ 包括哪些要素?
-
生产者 :消息的创建者,发送到RabbitMQ
-
消费者 :连接到RabbitMQ,订阅到队列上,消费消息,持续订阅(basicConsumer)和单条订阅(basicGet)
-
消息 :包含有效载荷和标签,有效载荷指要传输的数据,标签描述了有效载荷,并且RabbitMQ用它来决定谁获得消息,消费者只能拿到有效载荷,并不知道生产者是谁。
Q3:
RabbitMQ 什么是信道?
信道:是生产者、消费者与RabbitMQ通信的渠道,生产者publish或是消费者subscribe一个队列都是通过信道来通信的。信道是建立在TCP连接上的虚拟连接。就是说RabbitMQ在一条TCP上建立成百上千个信道来达到多个线程处理,这个TCP被多个线程共享,每个线程对应一个信道,信道在RabbitMQ都有一个唯一的ID,保证了信道私有性,对应上唯一的线程使用。
疑问:为什么不建立多个TCP连接?
原因是RabbitMQ需要保证性能,系统为每个线程开辟一个TCP是非常消耗性能的,美妙成百上千的建立销毁TCP会严重消耗系统性能;所以RabbitMQ选择建立多个信道(建立在TCP的虚拟连接)连接到RabbitMQ上
Q4:
RabbitMQ概念里的channel、exchange 和 queue是逻辑概念,还是对应着进程实体?作用分别是什么?
queue 具有自己的 erlang 进程;
exchange 内部实现为保存 binding 关系的查找表;
channel 是实际进行路由工作的实体,负责按照 routing_key 将 message投递给queue。
由 AMQP 协议描述可知,channel 是真实TCP连接之上的 虚拟连接 , 所有AMQP 命令都是通过 channel 发送的,且每一个 channel 有 唯一的ID 。一个 channel 只能被单独一个操作系统线程使用,所以投递到特定的 channel 上的 message 是有顺序的。单一个操作系统线程上允许使用多个channel。
Q5:
RabbitMQ消息是如何路由的?
消息路由必须有三部分:交换器、路由、绑定。
生产者把消息发布到交换器上,绑定决定了消息如何从路由器路由到特定的队列;消息最终到达队列,并被消费者接收。
消息发布到交换器时,消息将拥有一个 路由键(routing key) , 在消息创建时设定。
通过队列路由键,可以把队列绑定到交换器上。
消息到达交换器后,RabbitMQ会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则)。如果能够匹配到队列,则消息会投递到相应队列中;如果不能匹配到任何队列,消息将进入"黑洞"。
常用的交换器主要分为以下三种:
-
direct :如果路由键完全匹配,消息就会被投递到相应的队列;每个AMQP的实现都必须有一个direct交换器,包含一个空白字符串名称的默认交换器。声明一个队列时,会自动绑定到默认交换器,并且以队列名称作为路由键:channel -> basic_public($msg, '', 'queue-name')
-
fanout : 如果交换器收到消息,将会广播到所有绑定的队列上;
-
topic :可以使来自不同源头的消息能够到达同一个队列。使用topic交换器时,可以使用通配符,比如:"*" 匹配特定位置的任意文本,"." 把路由键分为了几个标识符, "#" 匹配所有规则等。
特别注意:发往topic交换器的消息不能随意的设置选择键(routing_key),必须是有"."隔开的一系列的标识符组成。
Q6:
RabbitMQ消息确认过程?
消费者收到的每一条消息都必须进行确认(自动确认和自行确认)
消费者在声明队列时,可以置顶autoAck参数,当autoAck = false时,RabbitMQ会等待消费者显式发送回 ack 信号后才从内存(和磁盘,如果是持久化消息的话)中删除消息,否则RabbitMQ会在队列中消息被消费后立即删除它。
采用消息确认机制后,只要使 autoAck = false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直持有消息直到消费者显式调用basicAck为止。
当autoAck = false时,对于RabbitMQ服务器端而言,队列中的消息分成了两部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者ack信号的消息。如果服务器端一直没有收到消费者的ack信号,并且消费此消息的消费者已经断开连接,则服务器端会安排该消息 重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)。
RabbitMQ不会为 ack消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开。这么设计的原因是RabbitMQ允许消费者消费一条消息的时间可以很久很久。
Q7:
如何保证RabbitMQ不被重复消费?
正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认信息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除。
但是因为网络传输等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
解决思路:
保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;
保证消息幂等性;
比如:在写入消息队列的数据做唯一标识,消费消息时,根据唯一标识判断该消息是否被消费过。
Q8:
如何保证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已收到消息;如果这时候处理消息失败,就会丢失该消息;
解决方案:处理消息成功后,手动回复确认消息。
点关注、不迷路
如果觉得文章不错,欢迎关注、点赞、收藏,你们的支持是我创作的动力,感谢大家。
如果文章写的有问题,请不要吝啬,欢迎留言指出,我会及时核查修改。
如果你还想更加深入的了解我,可以微信搜索「Java极客思维」进行关注。每天8:00准时推送技术文章,让你的上班路不在孤独,而且每月还有送书活动,助你提升硬实力!