java【消息队列】面试题
一、什么是消息队列?
消息队列,是分布式系统中重要的组件。
- 主要解决应用耦合,异步消息,流量削峰等问题。
- 可实现高性能,高可用,可伸缩和最终一致性架构,是大型分布式系统中不可缺少的中间件。
目前主流的消息队列有:
- Kafka
- RabbitMq
- RocketMq,老版本是MetaQ
- ActiveMq,目前用的人是越来越少了。
另外,消息队列容易和java中的本地MessageQuene搞混,所以消息队列更多的被称为消息中间件或者分布式消息队列等。
二、消息队列由哪些角色组成?
- 生产者(Producer):负责生产消息
- 消费者(Consumer):负责消费消息
- 消息代理(Message Broker):负责存储消息和转发消息两件事情。其中,转发消息分为推送和拉去两种方式。
- 拉取(Pull),是指Consumer主动从Message Borker获取消息
- 推送(Push),是指Message Broker主动将Consumer感兴趣的消息推送给Consumer。
三、消息队列有哪些使用场景?
一般来说,有四大应用场景:
- 应用解耦
- 异步处理
- 流量削峰
- 消息通讯
- 日志处理
其中,应用解耦、异步处理是比较核心的。
四、为什么使用消息队列进行应用解耦?
传统模式下,如下图所示:
- 缺点比较明显,系统间耦合性太强,系统A在代码中直接调用系统B和系统C的代码,如果将来D系统接入,系统A还需要修改代码,过于麻烦!并且万一系统A、B、C改接口,还要持续跟进。
引入消息队列,
- 将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而系统A不需要做任何修改。所以,有了消息队列之后,从主动调用的方式,变成了消息的订阅发布(或者说,事件的发布和监听),从而解耦。举个实际场景的例子,用户支付订单完成后,系统需要给用户发红包,增加积分等行为,就可以通过这样的方式进行解耦。
五、为什么使用消息队列进行异步处理?
- A系统需要串行逐个同步调用系统B、C、D。这其中会有很多问题。
- 如果每个系统调用执行是200ms,那么这个逻辑就要执行600ms。非常慢
- 如果任意一个系统调用异常报错,那个整个逻辑就报错了
- 如果任意一个系统调用超时了,那么整个逻辑就超时了。
- 。。。。。
引入消息队列后,如下图所示:
- 通过发送3条MQ消息,通过Consumer消费,从而异步,并行调用系统B、C、D
- 因为发送Mq消息是比较快的,假设每个操作2ms,那个这个逻辑只要执行6ms,非常快。
- 当然了,可能发送MQ消息会失败。当然,这个是会存在的。此时可以异步重试,当然,可能异步重试的过程中,JVM进程挂了,此时又需要其他的机制来保证,不过,相比串行逐个同步调用系统B、C、D来说,出错的几率会低很多。
六、为什么使用消息队列进行流量削峰?
对于大多数系统,一定会有访问量的波峰和波谷。比较明显的,就是我们经常使用的美团外卖,又或者被人诟病的小米秒杀。
如果在并发量大的时间,所有请求直接打到数据库,造成数据库直接挂掉
通过将请求先转发到消息队列汇总,然后,系统A慢慢的按照数据库能处理的并发量,从消息队列中逐步拉去消息进行消费。在生产中,这个暂时的高峰期积压是允许的。
相对来说,消息队列的性能会比数据库性能更好,并且,横向拓展能力更强。
七、为什么使用消息队列进行消息通信?
消息通讯是指:消息队列一般都内置了高效的通信机制,因此也可以 用在纯的消息通讯。
- IM 聊天。
- 点对点消息队列。可能大家会比较懵逼,有基于消息队列的 RPC 框架实现,例如 rabbitmq-jsonrpc ,虽然现在用的人比较少。
- 面向物联网的 MQTT 。阿里在开源的 RocketMQ 基础上,增加了 MQTT 协议的支持,可见消息队列 for IoT
八、如何使用消息队列进行日志处理?
日志处理:是指将消息队列应用在日志处理中,比如kafka的应用,解决大量日志传输的问题。
- 日志采集客户端,负责日志数据采集,定时批量写入kafka队列。
- kafka消息队列,负责日志数据的接收、存储和转发
- 日志处理应用:订阅并消费kafka队列中的日志数据
大家最熟悉的就是ELK+Kafka日志方案,如下:
- kafka:接收用户日志的消息队列。
- Logstash:对接kafka写入的日志,做日志解析,统一成Json输出给 Elasticsearch 中。
-
Elasticsearch :实时日志分析服务的核心技术,一个 schemaless ,实时的数据存储服务,通过 index 组织数据,兼具强大的搜索和统计功能。
- Kibna:基于Elasticsearch的数据可视化组件,超强的数据可视化能力是众多公司选择 ELK stack 的重要原因。
九、消息队列有什么优缺点?
任何中间件的引入,带来优点的时候,也同时会带来缺点。
- 系统可用性降低。
- 系统复杂度提高。
- 一致性问题。
- Producer发消息给message Broker阶段
-
- Producer发消息给message Broker时,不要求Messaage Broker对接收的消息响应确认,Producer也不用关心Message Broker是否收到消息了。
- Message Broker存储/转发阶段。
- 对Message Broker的存储不要求持久性
- 转发消息时,也不关心Consumer是否真的收到了。
- Consumer消费阶段
- Consumer从Message Broker中获取到消息后,可以从Message Broker删除消息。
- 或者Consumer Broker在消息被Consumer拿去消费时删除消息,不用关心Consumer最后对消息的消费情况如何。
2.消息至少被消费一次
适合不能容忍丢消息,允许重复消费的任务。
- Producer发送消息到Message Broker阶段
- Producer发消息给Message Broker,Message Broker必须响应对消息的确认。
- Message Broker存储/转发阶段
- Message Broker必须提供持久性的保障
- 转发消息时,Message Broker需要Consumer通知删除消息,才能将消息删除。
- Consumer消费阶段
- Consumer从Message Broker中获取消息,必须在消费完成后,Message Broker上的消息才能被删除。
3.消息仅被消费一次
适合对消费消费情况要求非常高的任务,实现较为复杂。
在这里需要考虑一个问题,就是这里的“仅被消费一次”指的是如下哪种情况:
- Message Broker上存储的消息被Consumer仅消费一次。
- Producer发送消息到Message Broker阶段
- Producer发消息给Message Broker时,不要求Message Broker对接收到的消息响应确认。Producer也不关心Message Broker是否收到消息了。
- Message Broker存储/转发阶段
- Message Broker必须提供持久性保障
- 并且,每条消息在其消费队列里有唯一标识(这个唯一标识可以由Producer产生,也可以由Message Broker产生)
- Consumer消费阶段
- Consumer从Message Broker中获取到消息后,需要记录下消费的消费标识,以便在后续消费中防止对某个消息重复消费(比如Consumer获取到消息,消费完后,还没有来得及从Messaage Broker删除消息九挂了,这样Message Broker如果把消息重新加入待消费队列的话,那么这条消息就会被重复消费了。)
- Producer发送消息到Message Broker阶段
- Producer上产生的消息仅被Consumer仅消费一次。
- Producer发送消息到Message Broker阶段
- Producer发消息到Message Broker时,Message Broker必须响应对消息的确认,并且Producer负责为该消息产生一个标识,以防止Consumer重复消费(因为Producer发消息给Message Broker后,由于网络问题没收到Message Broker的响应,可能会重发消息给高Message Broker)。
- Message Broker存储/转发阶段
- Message Broker必须提供持久性保障
- 并且,每条消息在其消息队列中有唯一标识(这一个标识需要由Producer产生)
- Consumer消费阶段
- Consumer仅被消费一次。
- Producer发送消息到Message Broker阶段
虽然3种方式看起来比较复杂,但是我们会发现,是层层递进,越来越可靠。
实际生产场景下,我们是倾向第三种的第二种情况,每条消息从Producer保证被送达,并且被Consumer仅消费一次。当然,重心还是如何保证Consumer仅消费一次。虽然说,消息产生的唯一标识可以在框架级去做排重,但也是最稳妥的,还是业务层也保证消费的幂等性。
十一、消息队列有几种投递方式?分别有什么缺点?
消息队列有两种投递方式:push推送和pull拉取
push:
- 优点:就是及时性
- 缺点:就是受限于消费者的消费能力,可能造成消息的堆积,Broker会不断的给消费者发送不能处理的消息。
pull:
- 优点:就是主动权掌握在消费者,可以根据自己的消费速度进行消息拉取。
- 缺点:就是消费方不知道什么时候可以获取最新的消息,会有消息延迟和忙等。
目前的消息队列,基于push+pull模式结合的方式,Broker仅仅告诉Consumer有新的消息,具体的消息拉取,还需要Consumer自己主动拉取。
十二、如何保证消费者的消息的幂等性?
如果要到达消费者的消息消息的幂等性,就需要消息仅被消费一次,且每一条消息从Producer保证被送达,并且被Consumer仅消费一次。
对于Producer来说:
- 可能因为网络问题,Producer重试多次发送消息,实际第一次就发送成功,那么就会产生多条相同的消息。
对于Consumer来说:
- 可能因为Broker的消息进度丢失,导致消息重复投递给Consumer。
- Consumer消费成功,但是因为JVM异常崩溃,导致消息的消费进度未及时同步给Consumer。
如何解决?
所以,上述的种种情况,都可能导致消费者会获取到重复的消息,那么我们的死来就无法是解决不发送、投递重复的消息,而是消费者在消费时,如何保证幂等性。
消费者实现幂等性,有两种方式:
1.框架层统一封装
2.业务层自己实现。
- 1.框架层统一封装
首先需要有一个消息排重的唯一标识,该编号由Producer生成,例如uuid,或者其他唯一编号的算法。
然后就需要一个排重的存储器,例如说:
-
使用关系数据库,增加一个排重表,使用消息编号作为唯一主键。
- 使用 KV 数据库,KEY 存储消息编号,VALUE 任一。此处,暂时不考虑 KV 数据库持久化的问题
- 在消息消费执行业务逻辑之前,插入这条排重记录。但是,此时会有可能 JVM 异常崩溃。那么 JVM 重启后,这条消息就无法被消费了。因为,已经存在这条排重记录。
- 在消息消费执行业务逻辑之后,插入这条排重记录。
- 如果业务逻辑执行失败,显然,我们不能插入这条排重记录,因为我们后续要消费重试。
-
如果业务逻辑执行成功,此时,我们可以插入这条排重记录。但是,万一插入这条排重记录失败呢?那么,需要让插入记录和业务逻辑在同一个事务当中,此时,我们只能使用数据库。
-
业务层自己实现
- 先查询数据库,判断数据是否已经被更新过。如果是,则直接返回消费完成,否则执行消费。
- 更新数据库时,带上数据的状态。如果更新失败,则直接返回消费完成,否则执行消费。
- …
ActiveMq
一般的业务系统要引入Mq,最早大家都用ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃
RabbitMQ
后来大家开始用RabbitMq,但是确实Erlang语言阻止了大量的java工程师去深入研究和掌控它,对公司几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,社区活跃度也高,另外,因为Spring Cloud在消息队列上的支持,对RabbitMq是比较不错的。所以在选型上又更加被推崇。
本文来自博客园,作者:King-DA,转载请注明原文链接:https://www.cnblogs.com/qingmuchuanqi48/p/13844227.html