消息队列面试系列-01

2022年7月17日
大约 16 分钟

消息队列面试系列-01

1. 什么是消息队列?

MQ全称为Message Queue 消息队列(MQ)是一种应用程序对应用程序的通信方法。

消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题。实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。

MQ是消费生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以读取队列中的消息。

消息生产者只需要把消息发布到MQ中而不用管谁来获取,消息消费者只管从MQ中获取消息而不管是谁发布的消息,这样生产者和消费者双方都不用清楚对方的存在。 

目前在生产环境,使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ等。

2. 为什么使用消息队列?

消息队列中间件有很多,使用的场景广泛。主要解决的核心问题是削峰填谷、异步处理、模块解耦。

3. RabbitMQ是什么?

RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。

RabbitMQ服务器是用Erlang语言编写的,而群集和故障转移是构建在开放电信平台框架上的。

目前所有主流的编程语言均有与代理接口通讯的客户端库。

4. 消息队列有哪些应用场景?

列举消息队列场景,异步处理,应用解耦,流量削锋,日志处理和消息通讯五个场景。

1、异步处理场景

用户注册后,需要发注册邮件和注册短信。传统的做法有两种

1)串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端。

2)并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间。

按照以上描述,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此采用消息队列的方式,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍。

2、应用解耦场景

用户购买物品下单后,订单系统需要通知库存系统。传统的做法是订单系统调用库存系统的接口。该模式的缺点:

1)假如库存系统无法访问,则订单减库存将失败,从而导致订单失败;

2)订单系统与库存系统耦合;

如何解决以上问题呢?

订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。

库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。

假如:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后订单系统写入消息队列就不再关心其他的后续操作,从而实现订单系统与库存系统的应用解耦。

3、流量削锋场景

流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中被广泛应用。

秒杀活动,一般会因为流量并发过大,导致流量暴增,应用宕机,从而为解决该问题,一般在应用前端加入消息队列。

控制活动的人数,缓解短时间内高流量压垮应用。当用户的请求,服务器接收后,先写入消息队列。如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面,而其秒杀业务根据消息队列中的请求信息,再做后续处理。

4、日志处理场景

日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。日志采集客户端,负责日志数据采集,定时写受写入Kafka队列,而Kafka消息队列,负责日志数据的接收,存储和转发,日志处理应用订阅并消费kafka队列中的日志数据。

5、消息通讯

消息通讯是指消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。

5. RabbitMQ 有哪些重要组件?

ConnectionFactory(连接管理器):应用程序与RabbitMQ之间建立连接的管理器

Channel(信道):消息推送使用的通道

Exchange(交换器):用于接受、分配消息

Queue(队列):用于存储生产者的消息

RoutingKey(路由键):用于把生产者的数据分配到交换器上

BindKey(绑定键):用于把交换器的消息绑定到队列上

6. RabbitMQ 有哪些重要角色?

生产者:

消息的生产者,负责创建和推送数据到消息服务器。

消费者:

消息的接收者,用于处理数据和确认消息。

代理:

RabbitMQ本身,用于扮演传递消息的角色,本身并不生产消息。

7. RabbitMQ 如何保证消息顺序性?

RabbitMQ保证消息的顺序性方式有两种:

1)拆分多个queue,每个queue对应一个consumer(消费者),就是多一些queue。

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

8. 如何保证消息消费的幂等性?

1、利用数据库的唯一约束实现幂等

比如将订单表中的订单编号设置为唯一索引,创建订单时,根据订单编号就可以保证幂等

2、去重表

根据数据库的唯一性约束类似的方式来实现。其实现大体思路是:首先在去重表上建唯一索引,其次操作时把业务表和去重表放在同个本地事务中,如果出现重现重复消费,数据库会抛唯一约束异常,操作就会回滚。

3、利用redis的原子性

每次操作都直接set到redis里面,然后将redis数据定时同步到数据库中。

4、多版本(乐观锁)控制

此方式多用于更新的场景下。其实现的大体思路是:给业务数据增加一个版本号属性,每次更新数据前,比较当前数据的版本号是否和消息中的版本一致,如果不一致则拒绝更新数据,更新数据的同时将版本号+1。

5、状态机机制

此方式多用于更新且业务场景存在多种状态流转的场景。

6、token机制

生产者发送每条数据时,增加一个全局唯一ID,这个ID通常是业务的唯一标识。在消费者消费时,需要验证这个ID是否被消费过,如果没消费过,则进行业务处理。处理结束后,在把这个ID存入Redis,同时设置状态为已消费。如果已经消费过,则清除Redis缓存的这个ID。

9. RabbitMQ 有几种广播类型?

direct(默认方式):最基础最简单的模式,发送方把消息发送给订阅方,如果有多个订阅者,默认采取轮询的方式进行消息发送。

headers:与direct类似,只是性能很差,此类型几乎用不到。

fanout:分发模式,把消费分发给所有订阅者。

topic:匹配订阅模式,使用正则匹配到消息队列,能匹配到的都能接收到。

10. RabbitMQ都有哪些特点?

可靠性

RabbitMQ使用一些机制来保证可靠性,如持久化、传输确认及发布确认等。

灵活的路由

在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。

扩展性

多个RabbitMQ节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。

高可用性

队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队列仍然可用。

多种协议

RabbitMQ除了原生支持AMQP协议,还支持STOMP,MQTT等多种消息中间件协议。

多语言客户端

RabbitMQ几乎支持所有常用语言,比如Java、Python、Ruby、PHP、C#、JavaScript等。

管理界面

RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。

插件机制

RabbitMQ提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。

11. 如何保证 RabbitMQ 消息队列的高可用?

RabbitMQ有三种模式:单机模式,普通集群模式,镜像集群模式。

单机模式:就是demo级别的,一般是指本地启动MQ,目前几乎没有公司生产中用单机模式。

普通集群模式:意思就是在多台机器上启动多个RabbitMQ实例,每个机器启动一个。

镜像集群模式:这种模式,才是所谓的RabbitMQ的高可用模式,跟普通集群模式不一样的是,用户创建的queue,无论元数据(元数据指RabbitMQ的配置数据)还是queue的消息都会存在于多个实例上,然后每次用户写消息到queue时都会自动把消息发送到多个实例的queue中进行消息同步。

12. RabbitMQ 中 broker 和 cluster 分别是指什么?

broker是指一个或多个erlang node的逻辑分组,且node上运行着RabbitMQ应用程序。

cluster是在broker的基础之上,增加了node之间共享元数据的约束。

13. vhost 是什么?有什么作用?

vhost可以理解为虚拟broker,即mini-RabbitMQ server。

其内部均含有独立的queue、exchange和binding等,但最重要的是其拥有独立的权限系统,可以做到vhost范围的用户控制。

当然,从RabbitMQ的全局角度,vhost可以作为不同权限隔离的手段。举一个典型的例子就是不同的应用可以跑在不同的vhost中。

14. RabbitMQ 中消息是基于什么传输?

由于TCP连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。

RabbitMQ使用信道的方式来传输数据。

信道是建立在真实的TCP连接内的虚拟连接,且每条TCP连接上的信道数量没有限制。

15. RabbitMQ 中如何解决丢数据的问题?

1、生产者丢数据

生产者的消息没有投递到MQ中怎么办?从生产者弄丢数据这个角度来看,RabbitMQ提供transaction和confirm模式来确保生产者不丢消息。

transaction机制就是说,发送消息前,开启事物(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事物就会回滚(channel.txRollback()),如果发送成功则提交事物(channel.txCommit())。

然而缺点就是吞吐量下降了。因此,按照博主的经验,生产上用confirm模式的居多。一旦channel进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个Ack给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了.如果rabiitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。

2、消息队列丢数据

处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。

那么如何持久化呢,这里顺便说一下吧,其实也很容易,就下面两步

1)将queue的持久化标识durable设置为true,则代表是一个持久的队列

2)发送消息的时候将deliveryMode=2

这样设置以后,rabbitMQ就算挂了,重启后也能恢复数据。在消息还没有持久化到硬盘时,可能服务已经死掉,这种情况可以通过引入mirrored-queue即镜像队列,但也不能保证消息百分百不丢失(整个集群都挂掉)

3、消费者丢数据

启用手动确认模式可以解决这个问题

1)自动确认模式,消费者挂掉,待ack的消息回归到队列中。消费者抛出异常,消息会不断的被重发,直到处理成功。不会丢失消息,即便服务挂掉,没有处理完成的消息会重回队列,但是异常会让消息不断重试。

2)手动确认模式,如果消费者来不及处理就死掉时,没有响应ack时会重复发送一条信息给其他消费者;如果监听程序处理异常了,且未对异常进行捕获,会一直重复接收消息,然后一直抛异常;如果对异常进行了捕获,但是没有在finally里ack,也会一直重复发送消息(重试机制)。

3)不确认模式,acknowledge="none" 不使用确认机制,只要消息发送完成会立即在队列移除,无论客户端异常还是断开,只要发送完就移除,不会重发。

16. 使用消息队列都有哪些缺点?

1、系统可用性降低

各个系统正常运行,而开发人员增加消息队列插件到项目中,如果此时消息队列服务挂了,那么系统也无法正常运行。因此,系统可用性也就降低了。

2、系统复杂性增加

系统中集成消息队列要考虑很多方面的问题,比如一致性问题、如何保证消息不被重复消费、如何保证保证消息可靠传输等问题。因此,需要考虑的各个方面问题更多,增大了系统复杂性。

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

在消息生产时,MQ内部针对每条生产者发送的消息生成一个inner-msg-id值,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列。

在消息消费时,要求消息体中必须要有一个bizId(对于同一业务全局唯一,比如支付ID、订单ID、帖子ID等)作为去重和幂等的依据,避免同一条消息被重复消费。

针对此类问题,列举处理消息的业务场景:

1、用户获取到消息执行数据库insert插入操作。那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。

2、用户获取到消息执行redis缓存set插入操作,不需要解决重复,因为无论set几次结果都是一样的,set操作本来幂等操作。

3、如果上面两种情况还不行,可以考虑使用记录消费。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以Key-Value的形式写入到redis中。当消费者开始消费消息时,首选获取redis中有没消费过该记录。

18. AMQP是什么?

AMQP(Advanced Message Queueing Protocol)协议是一个开放的标准的的协议,它定义了系统之间如何传递消息。

AMQP不仅定义了consumer、producer、broker之间如何交互,也定义了消息的格式和命令的交换。

RabbitMQ就是AMQP协议的Erlang的实现(当然RabbitMQ还支持STOMP2、MQTT3等协议)AMQP的模型架构和RabbitMQ的模型架构是一样的,生产者将消息发送给交换器,交换器和队列绑定。

RabbitMQ中的交换器、交换器类型、队列、绑定、路由键等都是遵循的AMQP协议中相应的概念。

目前RabbitMQ最新版本默认支持的是AMQP0-9-1。

19. AMQP 协议层都有哪些?

Module Layer:协议最高层,主要定义了一些客户端调用的命令,客户端可以用这些命令实现自己的业务逻辑。

Session Layer:中间层,主要负责客户端命令发送给服务器,再将服务端应答返回客户端,提供可靠性同步机制和错误处理。

TransportLayer:最底层,主要传输二进制数据流,提供帧的处理、信道服用、错误检测和数据表示等。

20. AMQP 模型有哪几大组件?

交换器(Exchange)

消息代理服务器中用于把消息路由到队列的组件。

队列(Queue)

用来存储消息的数据结构,位于硬盘或内存中。

绑定(Binding)

一套规则,告知交换器消息应该将消息投递给哪个队列。