主题
MQ 面试题
1.介绍消息队列的作用?
- 吞吐量提升:无需等待订阅者处理完成,响应更快速
- 故障隔离:服务没有直接调用,不存在级联失败问题
- 调用间没有阻塞,不会造成无效的资源占用
- 耦合度极低,每个服务都可以灵活插拔,可替换
- 流量削峰:不管发布事件的流量波动多大,都由 Broker 接收,订阅者可以按照自己的速度去处理事件
2.常见的 mq 产品?
| RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
|---|---|---|---|---|
| 公司/社区 | Rabbit | Apache | 阿里 | Apache |
| 开发语言 | Erlang | Java | Java | Scala&Java |
| 协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
| 可用性 | 高 | 一般 | 高 | 高 |
| 单机吞吐量 | 一般 | 差 | 高 | 非常高 |
| 消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
| 消息可靠性 | 高 | 一般 | 高 | 一般 |
追求可用性:Kafka、 RocketMQ 、RabbitMQ
追求可靠性:RabbitMQ、RocketMQ
追求吞吐能力:RocketMQ、Kafka(大吞吐量才会去使用)
追求消息低延迟:RabbitMQ、Kafka
3.rabbitmq 支持哪些消息模式?
- 基本消息队列 (使用默认转换器)
- 工作消息队列 (使用默认转换器)
- 发布订阅
- Fanout Exchange: 广播
- Direct Exchange: 路由
- Topic Exchange: 主题
4.什么是 AMQP 协议模型?

Broker:代表着一个中间件应用,负责接收消息生产者的消息,然后将消息发送至消息接受者或者其他的 broker。
Virtual host:这是对 broker 的虚拟化分,主要用于对 consumer、producer 和他们依赖的 AMQP 相关结构进行隔离。通常是处于安全因素的考虑。
Connection:代表着 producer、consumer 和 broker 之间的物理网络(TCP),connection 只有在客户端断开连接或者网络问题的时候会断开。
Channel:代表着 producer、consumer 和 broker 之间的逻辑连接,一个 Connection 可以包含多个 Channel。Channel 使得基同一连接的不同进程之间与 broker 之间的交互相互隔离,不干扰。而不需要重新建立连接,channel 在发生协议错误的时候会被关闭。
Exchange:这是所有被发送的消息首先到达的目的地,Exchange 负责根据路由规则将消息路由到不同的目的地。路由规则包括下面几种:direct(point-to-point)、topic(publish-subscribe)和 fanout(multicast)。
Queue:这是消息到达的最终目的地,到达 queue 的消息是已经准备好被消费的消息,一个消息可以被 exchange copy 发送至多个 queue。
Binding:这是 queue 和 exchange 之间的虚拟连接,使得消息从哪个 exchange 路由到 Queue。routing key 可以通过 binding 和 exchange routing 规则关联。
5.消息转换器的作用?
MessageConverter 的作用主要有两方面.
- 一方面它可以把我们的非标准化Message对象转换成我们的目标Message对象,这主要是用在发送消息的时候;
- 另一方面它又可以把我们的Message对象转换成对应的目标对象,这主要是用在接收消息的时候。
6.rabbitmq 如何保证消息的可靠性?

生产者弄丢消息时的解决方法
- 方法一:生产者在发送数据之前开启 RabbitMQ 的事务 (采用该种方法由于事务机制,会导致吞吐量下降,太消耗性能。)
- 方法二:开启 confirm 模式 (使用 springboot 时在 application.yml 配置文件中做如下配置,实现 confirm 回调接口,生产者发送消息时设置 confirm 回调)
- 小结: 事务机制和 confirm 机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是 confirm 机制是异步的,你发送个消息之后就可以发送下一个消息,RabbitMQ 接收了之后会异步回调 confirm 接口通知你这个消息接收到了。一般在生产者这块避免数据丢失,建议使用用 confirm 机制。
MQ 自身弄丢消息
- 第一步: 创建 queue 时设置为持久化队列,这样可以保证 RabbitMQ 持久化 queue 的元数据,此时还是不会持久化 queue 里的数据。
- 第二步: 发送消息时将消息的 deliveryMode 设置为持久化,此时 queue 中的消息才会持久化到磁盘。
- 总结:同时设置 queue 和 message 持久化以后,RabbitMQ 挂了再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据,保证数据不会丢失。
- 但是:但是就算开启持久化机制,也有可能出现上面说的的消息落盘时服务挂掉的情况。这时可以考虑结合生产者的 confirm 机制来处理,持久化机制开启后消息只有成功落盘时才会通过 confirm 回调通知生产者,所以可以考虑生产者在生产消息时维护一个正在等待消息发送确认的队列,如果超过一定时间还没从 confirm 中收到对应消息的反馈,自动进行重发处理
消费者弄丢消息
- 方法:关闭自动 ACK,使用手动 ACK。RabbitMQ 中有一个 ACK 机制,默认情况下消费者接收到到消息,RabbitMQ 会自动提交 ACK,之后这条消息就不会再发送给消费者了。我们可以更改为手动 ACK 模式,每次处理完消息之后,再手动 ack 一下。不过这样可能会出现刚处理完还没手动 ack 确认,消费者挂了,导致消息重复消费,不过我们只需要保证幂等性就好了,重复消费也不会造成问题。
- 步骤一:在 springboot 中修改 application.yml 配置文件更改为手动 ack 模式
- 步骤二:手动实现 ack 的 callback
总结

7.rabbitmq 如何避免消息堆积?
1.去优化消费者代码,提高消费能力。减少消费时间
2.可以给消费设置年龄(生命周期),如果超时就丢弃掉。可以不让消息大量堆积在消息队列中
3.可以设置队列的最大长度:如果超过了,就无法接收消息到队列中。
4.建立新的消息队列,采用订阅模式,消费者同时去订阅新的,还有旧的消息队列,同时去消费消息。
8.rabbitmq 如何防止消息重复消费?
每个消息用一个唯一标识来区分,消费前先判断标识有没有被消费过,若已消费过,则直接ACK
9.rabbitmq 如何保证高可用
镜像集群模式
10.rabbitmq 在项目中的使用场景
面试题
1、RabbitMQ 工作模式
五种模式:
- 简单模式:一对一模式,一个生产者、一个消费者,一个队列,生产者发送消息,消费者消费消息
- 工作队列模式:一对多模式,一个生产者,多个消费者,一个队列,每个消费者从队列中获取唯一的消息。与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息
- 发布订阅模式:

多了一个 Exchange 角色,而且过程略有变化:
生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给 X(交换机)
消费者,消息的接收者,会一直等待消息到来
消息队列,接收消息、缓存消息
交换机一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于 Exchange 与消息队列的绑定模式:
- F****anout:广播,将消息交给所有绑定到交换机的队列,当发送一条消息到 fanout 交换器上时,它会把消息投放到所有附加在此交换器上的队列
- Direct:定向,把消息交给符合指定 routing key 的队列,如果路由键完全匹配的话,消息才会被投放到相应的队列
- Topic:通配符,把消息交给符合 routing pattern(路由模式) 的队列,设置模糊的绑定方式,“*”操作符将“.”视为分隔符,匹配单个字符;“#”操作符没有分块的概念,它将任意“.”均视为关键字的匹配部分,能够匹配多个字符
交换机:只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失
工作模式总结:
这五种工作模式,可以归结为 3 类:
生产者,消息队列,一个消费者;
生产者,消息队列,多个消费者;
生产者,交换机,多个消息队列,多个消费者
2、MQ 如何保证顺序消费
RabbitMQ 的 queue 本身就是队列,是可以保证消息的顺序投递的。
但是消息的顺序消费则是另一回事了,所谓的“顺序消费”意味着是否顺序达到目的地,比如:数据库。
看看以下场景:
一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者 2 先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。

产生多个 consumer 去消费一个 queue,极有可能是因为:消息消费太慢,所以盲目让多个 consumer 同时来消费,而忽略了消息消费顺序性。
在某些情况下,消息是需要保证顺序性的,如果上图中的 data1, data2, data3 分别意味着对某条数据的增改删,但是如果乱序以后就变成了:删改增。
解决方案:

3、MQ 如何消息不丢失,持久化
有四种方式可以解决:
- 消息持久化
- ACK 确认机制(项目中使用):ACK 确认机制是消费者从队列中收到消息,进行业务逻辑的处理,在这一过程中如果消费者出现服务器异常、网络不稳定等,都不会有 ACK 应答,这时候 RabbitMQ 就认为这条消息没有正常消费成功,就会将消息重新放回队列中。直到消费者发送 ACK 应答,这条消息才会从队列中消费掉。使用:需要手动开启配置,yaml 文件
publisher-confirm-type: correlated # 开启确认机制回调 必须配置这个才会确认回调
publisher-returns: true # 开启return机制回调
- 设置集群镜像模式
- 消息补偿机制
消息持久化解决:
消息中心收到生产者的消息后,先将消息存储在本地数据文件,内存数据库或者远程数据库,再试图把消息发送给消费者,发送成功则讲消息从存储中删除,如失败则继续尝试发送
消息中心启动时,先会检查指定的存储位置,如有未成功发送的消息,则会把消息发送出去
- Exchange 设置持久化:基于代码的,参数设为 true
- Queue 设置持久化
- Message 持久化发送:发送消息设置发送模式 deliveryMode=2,代表持久化消息
4、MQ 的消息确认机制(生产者发送到交换机,有个回调,交换机到队列,也有个回调,消费者有没有去消费这个消息)
- 为了保证消息从队列可靠的达到消费者,RabbitMQ 提供了消息确认机制(Message Acknowledgement)。消费者在订阅队列时,可以指定 autoAck 参数,当 autoAck 参数等于 false 时,RabbitMQ 会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移除消息(实际上是先打上删除标记,之后在删除)。当 autoAck 参数等于 true 时,RabbitMQ 会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了这些消息。
- 采用消息确认机制后,只要设置 autoAck 参数为 false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为 RabbitMQ 会一直等待持有消息直到消费者显式调用 Basic.Ack 命令为止。
- 当 autoAck 参数为 false 时,对于 RabbitMQ 服务器端而言,队列中的消息分成了两部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者确认信号的消息。如果 RabbitMQ 服务器端一直没有收到消费者的确认信号,并且消费此消息的消费者已经断开连接,则服务器端会安排该消息重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)。
- RabbitMQ 不会为未确认的消息设置过期时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息连接是否已经断开,这个设置的原因是 RabbitMQ 允许消费者消费一条消息的时间可以很久很久。
5、MQ 高可用
镜像集群模式:

- 无论元数据还是 queue 里的消息都会存在于多个 broker 上
- 每个 queue 都想拥有多个镜像放在其他 broker 上,可以选择镜像队列的数量
- 由于每个 broker 上都具有近乎完整的数据,所以消费者消费的时候并不需要进行消息传输,但由于并不是想 Kafka 分布式消息队列那样的分片存储,所以性能并不高
小小总结一下,RabbitMQ 其实并不是分布式消息队列,大厂使用的分布式消息队列,更多是 RocketMQ 或者 Kafka,可以分布式分片存储,水平扩容性能会有明显提升
6、消息生产方将消息成功投递到消息消费方,消息消费成功了, 业务逻辑执行失败了, 问怎么办?
自动确认机制 改成 手动确认,在业务逻辑 try...catch{} 拒绝接收,消息重放到 MQ 队列
7、MQ 消息堆积问题
多线程消息 + 一个线程一次去消费多少条消息
8、如何保证幂等性
- 消费数据为了单纯的写入数据库,可以先根据主键查询数据是否已经存在,如果已经存在了就没必要插入了。或者直接插入也没问题,因为可以利用主键的唯一性来保证数据不会重复插入,重复插入只会报错,但不会出现脏数据。
- 消费数据只是为了缓存到 redis 当中,这种情况就是直接往 redis 中 set value 了,天然的幂等性。
- 针对复杂的业务情况,可以在生产消息的时候给每个消息加一个全局唯一 ID,消费者消费消息时根据这个 ID 去 redis 当中查询之前是否消费过。如果没有消费过,就进行消费并将这个消息的 ID 写入到 redis 当中。如果已经消费过了,就无需再次消费了。key:全局唯一 redis:key
9、消费者已经接收了消息,因为某种原因导致业务代码没执行完全,怎么处理?
- 失败重试机制,配置

- 策略
