又到一年跳槽的高峰期啦,各位小伙伴有没有跳槽涨工资的打算呢。既然要准备找工作面试了,java八股文可要好好复习一下了。这篇文章汇总一下所有与MQ相关的面试问题及相关知识点,拿去应付面试足够啦。面试官也未必懂得比这里的介绍得多。毕竟,谁会把用Scala语言写的kafka,Erlang语言写的rabitmq 源码全部看一遍呢?

与mq相关的面试无非这几个点:push/pull模式、消息模式(点对点、发布/订阅者模型),调用方式(同步调用,异步调用),ACK模式,消息重复问题,消息顺序消费问题等。

1. 为什么用MQ

为了系统解耦、风险或故障隔离、流量削峰、提高数据的安全性等等。

2. 消息模型(JMS规范,基本概念)

消息模型指的是消息中间件如何处理每一条消息,是1对1的还是1对多,这是在JMS中定义的。目前共有2种消息模型:1)点对点模型(基本概念是queue,但是很多mq组件还是把它叫做topic);2)发布订阅模型(topic)

点对点模型(queue)

在点对点模型(Queue)中,消息不可重复消费。生产者发送消息到queue,然后消费者从queue取出消息并且消费。消息被消费以后,queue中不再存储,所以消费者不可能消费到已经被消费的消息(重复消费问题)。Queue可以有多个消费者,但是一条消息只被一个消费者消费。

发布订阅模型 (topic)

在发布/订阅模型中,消息是发送到topic中并且可以重复消费的。生产者将消息发布到topic中,多个消息消费者(订阅)消费该消息。 发布到topic的消息会被所有订阅者消费。

订阅组

当发布者消息量很大时,单个订阅者的处理能力可能不足。所以在实际应用中,一般设置多个订阅者组成一个订阅组,在负载均衡机制的保护下,共同消费topic中的消息,即分组订阅。这样能实现消费能力扩展。

ActiveMQ遵循JMS规范,实现了点对点和发布订阅模型。RabbitMQ和Kafka没有遵循JMS规范。

3. 推拉模式(基本概念)

对于任何一种MQ中间件来说,消费者有两种方式获取消息。

① Push方式:由消息中间件主动地将消息推送给消费者;

② Pull方式:由消费者主动向消息中间件拉取消息。

采用Push方式,可以尽可能快地将消息发送到消费者。采用Pull方式,消息会有一点延迟,即消息到达消费者的时间有点长。如果想要一条消息尽快从生产者发送到消费者(高实时性),那么应该采用push方式,即消息中间件主动把消息push到消费端。

Push方式的缺点是,如果消费者的处理能力弱而消息中间件不断地向消费者Push消息,消费者的缓冲区可能溢出。解决办法就是增加prefetch limit,规定一次可以Push多少条消息。当推送消息的数量达到perfetch limit时,且消费者还没有返回ACK,消息中间件就不再继续推送消息。pull方式,消费者可以根据自身处理能力去拉取消息,因此不存在这个问题。

RabbitMQ消费者默认是push模式(也支持pull模式),kafka默认是pull模式。

4. 消费端调用方式(基本概念)

在消费端应用程序中如何获取消息呢?有两种方式获取消息,即同步调用方式和异步调用方式。这是从API调用是否阻塞的角度看的。同步方式使用的是ActiveMQMessageConsumer的receive()方法。而异步方式则是消费者实现一个MessageListener接口(回调机制),监听消息。

同步方式:同步方式即直接在代码中调用同步方法receive()获取消息。如果本地缓存队列中有消息,则立即返回,否则就会阻塞等待直到有消息为止。

异步方式:异步方式即我们去实现一个java interface(回调接口),然后注册到mq组件。如果接收到mq,则我们的回调接口就会被调用。

5. ACK模式

在MQ中,每一条消息都需要ACK,确认这条消息是否消费成功。broker接收到ACK指令才会认为消息被正确处理。如何对消息进行ACK又是消息中间件需要考虑的一个问题。消费者以何种方式向消息中间件返回确认ACK(响应)。消费者是每次消费一条消息之后就向消息中间件确认呢,还是采用“延迟确认”?客户端接收到消息且处理完成以后,是否需要再调用一个API告诉broker消费成功。这就是ACK模式

JMS约定客户端可以使用四种ACK模式(见javax.jms.Session接口)。

  • AUTO_ACKNOWLEDGE = 1 自动确认
  • CLIENT_ACKNOWLEDGE = 2 客户端手动确认
  • DUPS_OK_ACKNOWLEDGE = 3 自动批量确认
  • SESSION_TRANSACTED = 0 事务提交并确认

ActiveMQ补充了一个ACK模式:

  • INDIVIDUAL_ACKNOWLEDGE = 4 单条消息确认

1)AUTO_ACKNOWLEDGE (自动确认)

如果指定成这种ACK模式,consumer将自行选择时机对消息进行ACK,不需要我们手动调用message.acknowledge()方法。

ACK模式一般是在客户端中指定,但是Client与broker在交换ACK指令的时候,还需要告知ACK_TYPE(ACK_TYPE表示此确认指令的类型),broker可以根据不同的ACK_TYPE对消息进行不同的操作。例如当客户端消费消息时出现异常,就向broker发送ACK指令,ACK_TYPE为”REDELIVERED_ACK_TYPE”,那么broker就重新发送此消息。

2)CLIENT_ACKNOWLEDGE (客户端手动确认)

客户端手动确认,这意味着MQ不会“自动”ACK任何消息,开发者需要手动确认,即调用message.acknowledge()。如果client端未确认的消息个数达到prefetchSize * 0.5时,会补充发送一个ACK_TYPE为DELIVERED_ACK_TYPE的确认指令,触发broker端可以继续push消息到client端。

注意防止不ack而hang住:如果client端因为某种原因导致acknowledge方法未被执行,将导致大量消息不能被确认,broker端将不会push消息,client端将处于“假死”状态,无法继续消费消息。

broker依据ack速率进行负载平衡:在CLIET_ACK模式下,消息在交付给listener之前,都会首先创建一个DELIVERED_ACK_TYPE的ACK指令,直到client端未确认的消息达到”prefetchSize * 0.5″时才会发送此ACK指令;如果在此之前,开发者调用了acknowledge()方法,会导致消息直接被确认。

broker端通常会认为“DELIVERED_ACK_TYPE”确认指令是一种“slow consumer”信号,如果consumer不能及时对消息进行acknowledge而导致broker端阻塞,那么此consumer将会被标记为“slow”,此后queue中的消息将会转发给其他Consumer。

3)DUPS_OK_ACKNOWLEDGE(自动批量确认)

这个模式的字面意思是“重复可接受”,也就是说允许消息重复消费。它是一种潜在的”AUTO_ACK”确认机制,为批量确认而生,具有“延迟”确认的特点。

对于开发者而言,这种模式下的代码结构和AUTO_ACKNOWLEDGE一样,不需要像CLIENT_ACKNOWLEDGE那样调用acknowledge()方法来确认消息。

发生作用的时机:1) 在ActiveMQ中,如果在Destination是Queue通道,我们可以认为DUPS_OK_ACK就是“AUTO_ACK + optimizeACK + (prefetch > 0)” 这种情况;此外在此模式下,如果prefetchSize =1 或者没有开启optimizeACK,也会导致消息逐条确认,失去批量确认的特性。

2) 如果Destination为Topic,DUPS_OK_ACKNOWLEDGE才会产生JMS规范中诠释的意义,即无论optimizeACK是否开启,都会在消费的消息个数>=prefetch * 0.5时,批量确认。在此过程中,不会发送DELIVERED_ACK_TYPE的确认指令,这是DUPS和AUTO_ACK的最大的区别。所以当consumer故障重启后,那些尚未ACK的消息会重新发送过来。

4)SESSION_TRANSACTED (事务提交并确认)

当session使用事务时,就是使用此模式。事务开启后且在session.commit()之前,所有消息要么全部正常确认,要么全部redelivery。

因为session非线程安全,那么当前session下所有的consumer都会共享同一个transactionContext;还要注意,一个事务类型的Session中只可以有一个consumer,以避免rollback()或commit()方法被多个consumer调用而造成消息混乱。

事务的确认过程:首先把本地deliveredMessage队列中尚未确认的消息全部确认;然后向broker发送transaction提交指令并等待broker反馈,如果broker端事务操作成功,那么将会把本地deliveredMessage队列清空,事务成功;如果broker端事务操作失败(此时broker已经rollback),那么执行inner-rollback,这个rollback所做的事情,就是将当前事务中的消息清空并要求broker重发,同时commit方法抛出异常。

5)INDIVIDUAL_ACKNOWLEDGE (单条消息确认)

用得比较少,确认时机和CLIENT_ACKNOWLEDGE几乎一样。当消费成功之后,需要手工调用API message.acknowledege()来确认此条消息。在CLIENT_ACKNOWLEDGE模式中,调用message.acknowledge()方法将导致整个session中的所有消息被确认。

ActiveMQ定义了如下几种ACK_TYPE:

  • DELIVERED_ACK_TYPE = 0 消息”已接收”,但尚未处理结束
  • STANDARD_ACK_TYPE = 2 “标准”类型,通常表示为消息”处理成功”,broker端可以删除消息
  • POSION_ACK_TYPE = 1 消息”错误”,通常表示”抛弃”此消息,比如消息重发多次后,都无法正确处理时,消息将会被删除或者DLQ(死信队列)
  • REDELIVERED_ACK_TYPE = 3 消息需”重发”,比如consumer处理消息时抛出了异常,broker稍后会重新发送此消息
  • INDIVIDUAL_ACK_TYPE = 4 表示只确认”单条消息”,无论在任何ACK_MODE下
  • UNMATCHED_ACK_TYPE = 5 在Topic中,如果一条消息在转发给“订阅者”时,发现此消息不符合Selector过滤条件,那么此消息将不会转发给订阅者,消息将会被存储引擎删除(相当于在Broker上确认了消息)。

RabitMQ

基本特点必须记住哦):RabbitMQ实现了AMQP协议,没有遵守 JMS规范,使用Erlang语言实现,默认采用点对点模型,消费端默认采用推模式(push),也支持pull模式,消息实时性高,可靠性好,不会重复消费,但是吞吐率不高。

RabitMQ默认使用点对点模型(也支持发布订阅),生产端通过路由规则发送消息到不同queue,消费端根据queue名称消费消息。RabbitMQ既支持内存队列也支持持久化队列,消费端为推模型,消费状态和订阅关系由服务端负责维护,消息消费完后立即删除。

当RabbitMQ需要支持多订阅时,发布者发送的消息通过路由同时写到多个Queue,不同订阅组消费不同的Queue。所以多订阅时,消息会被拷贝多份。

Kafka

基本特点(必须记住哦):Kafka支持消息持久化,消费端默认采用pull模式,消费状态和订阅关系由客户端维护,消息被消费完成后不会立即删除,会保留历史消息。因此支持多订阅时,消息只存储一份,但是可能重复消费。

kafka点对点或多订阅模式

kafka发布者生产一条消息到topic,不同订阅组消费此消息,消费组自己记录消费成功状态,且消费以后不会立即删除topic中的消息。

ActiveMQ

下面这篇文章针对ActiveMQ的推拉模型进行介绍。
http://www.cnblogs.com/hapjin/p/5683648.html

ActiveMQ消息传送机制以及ACK机制详解
http://shift-alt-ctrl.iteye.com/blog/2020182

JMS

Java Message Service(JMS)指的是面向消息的中间件(MOM),用于在两个应用程序之间或分布式系统中发送消息,从而进行异步通信。

JMS定义了两种消息模型:点对点(point to point, queue)和发布/订阅(publish/subscribe,topic)。主要区别就是能否重复消费。

AMQP

AMQP最开始是为金融业提供的一个彼此协作的消息协议,现在则成为通用消息队列架构的通用构建工具。AMQP中的AMQ实体形成在线路层协议顶端的一个AMQP模型。这个模型统一了消息模式,诸如发布/订阅,队列,事务以及stream数据。

简单说来,AMQP就是一种消息中间件的规范,ActiveMQ和RabbitMQ均实现了AMQP。

最后,只要把MQ中的消息模型(点对点vs发布订阅)、推拉模型、同步异步调用、ACK模式弄明白,再加上一点变化,研究消息重复消费问题、消息按顺序消费问题是怎么做到的,java面试被问到mq的问题应该就差不多啦。

希望这篇文章能对您有帮助!如果您对互联网、前/后/客户端、架构/分布式/高可用/高并发/高实时、电商、Redis、MySQL、Zookeeper、Spring、Android、浏览器插件、Java、C/C++、Linux、个性化推荐、社区发现、机器学习、数据挖掘等感兴趣,欢迎关注,私信领取更多Java学习资料。