MQ篇面试题
# RabbitMQ如何实现异步通讯的
1、生产者将消息发送到交换机exchange
2、交换机根据指定的规则(交换机类型:fanout/广播,direct/路由,topic/主题)把消息路由到队列
3、消费者监听队列消费消息
# 为什么要在项目中使用MQ?
面试话术
MQ的作用主要是解耦、异步和削峰,比如业务的远程调用,如果用feign来远程调用的话,就需要将代码写死,而且如果需要扩展调用接口的话,需要修改的系统代码也会非常多,而使用MQ消息队列的话只需要修改发送的消息,比如路由Key啊,交换机之类的,一定程度上降低了服务间的代码耦合度。然后MQ发送消息还有异步的特点,效率也会比较快, 但实时性就相对会弱一些。当然业务中一般这两个都会用,feign的远程调用和MQ的远程调用,比如需要响应的调用就得使用feign,就那些需要返回值的,而不需要响应的,仅仅只是作为通知的那些调用就可以使用MQ了。
另外MQ还能做到流量削峰的效果,比如类似秒杀的高并发场景,大量的请求直接打到数据库容易导致数据库崩溃,MQ就可以使用延迟队列,让消息经过一定时间后再被消费,排队执行,从而降低对数据库的压力。
# 如何保证MQ消息不丢失 (可靠性)?
RabbitMQ针对消息传递过程中可能发生问题的各个地方,给出了针对性的解决方案
- 生产者发送消息时可能因为网络问题导致消息没有到达交换机:
- RabbitMQ提供了publisher confirm机制
- 生产者发送消息后,可以编写ConfirmCallback函数
- 消息成功到达交换机后,RabbitMQ会调用ConfirmCallback通知消息的发送者,返回ACK
- 消息如果未到达交换机,RabbitMQ也会调用ConfirmCallback通知消息的发送者,返回NACK
- 消息超时未发送成功也会抛出异常
- RabbitMQ提供了publisher confirm机制
- 消息到达交换机后,如果未能到达队列,也会导致消息丢失:
- RabbitMQ提供了publisher return机制
- 生产者可以定义ReturnCallback函数
- 消息到达交换机,未到达队列,RabbitMQ会调用ReturnCallback通知发送者,告知失败原因
- RabbitMQ提供了publisher return机制
- 消息到达队列后,MQ宕机也可能导致丢失消息:
- RabbitMQ提供了持久化功能,集群的主从备份功能
- 消息持久化,RabbitMQ会将交换机、队列、消息持久化到磁盘,宕机重启可以恢复消息
- 镜像集群,仲裁队列,都可以提供主从备份功能,主节点宕机,从节点会自动切换为主,数据依然在
- RabbitMQ提供了持久化功能,集群的主从备份功能
- 消息投递给消费者后,如果消费者处理不当,也可能导致消息丢失
- SpringAMQP基于RabbitMQ提供了消费者确认机制、消费者重试机制,消费者失败处理策略:
- 消费者的确认机制:
- 消费者处理消息成功,未出现异常时,Spring返回ACK给RabbitMQ,消息才被移除
- 消费者处理消息失败,抛出异常,宕机,Spring返回NACK或者不返回结果,消息不被异常
- 消费者重试机制:
- 默认情况下,消费者处理失败时,消息会再次回到MQ队列,然后投递给其它消费者。Spring提供的消费者重试机制,则是在处理失败后不返回NACK,而是直接在消费者本地重试。多次重试都失败后,则按照消费者失败处理策略来处理消息。避免了消息频繁入队带来的额外压力。
- 消费者失败策略:
- 当消费者多次本地重试失败时,消息默认会丢弃。
- Spring提供了Republish策略,在多次重试都失败,耗尽重试次数后,将消息重新投递给指定的异常交换机,并且会携带上异常栈信息,帮助定位问题。
- 消费者的确认机制:
- SpringAMQP基于RabbitMQ提供了消费者确认机制、消费者重试机制,消费者失败处理策略:
面试话术
我记得RabbitMQ提供很多种检测消息是否成功消费的解决方案,比如要保证消息投递的可靠性可以利用生产者确认机制,正常的话生产者发送消息到MQ,MQ会给生产者发送ACK确认,我们可以通过comfirmCallback回调函数来获取这个ACK,如果ACK为true就是投递成功,如果为false就是投递失败,我们就可以进行一个重投。
然后成功投递到交换机但是没能到达队列的话可以利用publisher return机制,就是生产者返回?机制,通过ReturnCallback回调函数获取失败原因,然后我们同样可以做重投。
然后可以设置MQ的持久化,就是交换机队列那些,防止MQ服务器出现宕机导致数据丢失,不过我记得spring整合rabbitMQ的API默认都是开启的,好像还可以搭建集群,做队列的主从备份。
然后消费者也是有确认机制的,配置消费者的时候默认都是开启auto自动确认模式,消费者只有在执行逻辑成功后才会发送ACK给MQ,让MQ移除消息,执行失败的话会重新将消息放回MQ投递给其他消费者。然后也可以配置消息消费的重试次数,以及间隔时间。
但是消费者的ACK确认的话就比较影响效率,每一个业务都要等待ACK确认才算完成,会影响并发量,所以最好直接存到表里面,消息生产者做到最大努力通知,消费者则接受到消息后先不消费,先存表,存起来后再去进行一个消费逻辑,具体消费成功与否,可以自己写重试逻辑,或者补偿之类的。
# 如何保证消息不被重复消费(幂等性)?
面试话术
不被重复消费就是要做去重判断嘛,去重肯定得做个唯一标识,就是给每条消息分配一个全局唯一ID,用雪花算法之类的。然后有一种方案的话是利用MySQL做去重,建一张表,设置消息ID字段为unique,然后每次消费消息前带着ID去对这张表做插入数据的操作,如果报错则说明表中有了,那就不去做消费的逻辑而是直接结束,若是成功则说明第一次执行这条消息,正常执行就好。不过这种并发度就比较有限,而且会加大MySQL的负担。
所以我其实更推荐用Redis来做这个判断,利用Redis的全局锁,就是setnx那个指令,只有当数据不存在的时候才能成功将key set进去,就以当前消息的唯一id为key,然后使用redistemplate的那个setIfAbabsent方法,在执行消费逻辑前将这个key存入,返回一个布尔,如果为true则代表消息没有被消费过,false则说明消费过,那就直接返回不走消费逻辑。用Redis的话就不用担心会影响MySQL而且并发度也更高。
# 请问什么是死信队列?项目中一般用来做什么?
面试话术
死信队列就是被拒绝的消息或者TTL到期的消息以及由于队列已满没能成功进入队列的消息,最终都会到达死信队列进行处理。
我记得我在项目里面用死信队列是做一个超时订单处理的需求,设置一个延时队列TTL为半个小时,然后订单创建时就会携带订单id发送消息到这个队列中,等TTL时间到了,就会加入死信队列,然后死信队列的消费者就通过id去查这个订单,判断订单状态是否未支付,未支付就做一个取消订单的处理。就这类的延时任务都可以用死信队列来做。
# 如何解决MQ消息堆积问题?
面试话术
消息堆积也就是说要提高消息的处理效率,那就可以加多几个消费者嘛,以及搞个异步执行消费逻辑,甚至是搭建一个MQ集群也都能够达到这个效果,集群的话就相当于是加大消息的存储空间了嘛,以及RabbitMQ1.8后我记得加入了一个新的队列叫Lazy Queue,这个队列的消息是硬盘存储不是内存存储,所以理论上这种队列的消息存储是无上限的,就是硬盘存储的话会有IO性能的限制,消息实时性可能会有一定的影响。
# RabbitMQ如何保证消息的有序性?
面试话术
RabbitMQ本身就是队列存储嘛,先进先出,所以一个队列的消息本身就是顺序消费的,不过如果有多个消费者的话就会有一些预取啊或者轮询的机制导致消息投递顺序打乱,所以我们要控制的就是发送消息进队列和出队列的一个消息的有序,发送就用同步嘛,不要用异步,消费的话就只用一个消费者,不要绑定多个,这样就能保证消息的有序性了,但是这种时候就有可能会出现消息堆积的情况,有点像是那种鱼和熊掌不可兼得。
# 如何保证Kafka的消息是有序的?
面试话术
kafka有个特性就是的同一个分区的消息都是有序的,那我们就可以控制一个topic,一个主题的分区数量为1,那么这个分区的消息就都是有序的了,以及向这个分类发送消息的时候也是尽量用同步的方式,就不要用异步,异步可能会因为网络贷款或者没抢到cpu时间片之类的导致顺序错掉这样,不过一个分区的话就会对kafka的并发能力有所损耗,毕竟卡夫卡并发能力强的其中一个原因就是因为这个分区嘛,所以这个只能二选一了,在一个性能跟消息的有序之间。不过kafka其实一个分区也比其他MQ要快,我记得之前有看过一个测试数据,kafka相当于RabbitMQ的一个近十倍的效率。
# 为什么Kafka那么快?
面试话术
首先就是kafka的一个设计吧,他是规定一个主题可以有多个partition分区嘛,然后分区是并行执行的,所以效率自然就高了。然后kafka好像还有利用Linux的一个page cache的机制,他会将磁盘擦写的命令缓存起来,到一定量后再做一个批量处理,就减少了磁盘擦写的频率,然后也有利用到零拷贝技术来减少cpu的开销,这里也是用到IO缓冲,减少数据拷贝的频次。然后kafka对消息也做批处理,假设我突然要发1w条消息,他会对消息做一个缓冲,分10批一次发1k条这样,同时也有对消息的数据进行一个压缩,每个消息数据小了,网络带宽占用的也少,效率自然也就上去了。
# 你们项目中为什么选了Kafka而不是RabbitMQ?
面试话术
呃...具体的原因我也不是很清楚,我个人的理解的话Kafka就是以高吞吐量闻名嘛,说是百万级,然后Kakfa是依赖与zookeeper的,Kafka的稳定性也不错,而且运用的领域广,好像在大数据也有用而且Kafka我记得还有一个KafkaStream来做流量削峰。但是支持的语言比较少了,其他的话依赖zookeeper可能也算是缺点吧,要耦合其他技术。RabbitMQ的话我其实觉得更灵活轻便一点,他不需要依赖其他技术,支持的语言也很广,稳定性也不错,然后RabbitMQ官方好像还提供了是五种消息发送模式,我们用起来也比较方便, 吞吐量的话不算多不算少吧,万级,不过源码好像是Erlang写的,就可能要研究源码的话,学习成本比较高,我个人的话其实觉得RabbitMQ也够用了。