【我想进大厂!】超全面试题总结.消息队列MQ篇

81 阅读10分钟

 消息队列MQ

你们为什么使⽤mq?具体的使⽤场景是什么?

mq的作⽤很简单,削峰填⾕。以电商交易下单的场景来说,正向交易的过程可能涉及到创建订单、扣减库存、扣减活动预算、扣减积分等等。每个接⼝的耗时如果是100ms,那么理论上整个下单的链路就需要耗费400ms,这个时间显然是太⻓了。​编辑

如果这些操作全部同步处理的话,⾸先调⽤链路太⻓影响接⼝性能,其次分布式事务的问题很难处理,这时候像扣减预算和积分这种对实时⼀致性要求没有那么⾼的请求,完全就可以通过mq异步的⽅式去处理了。同时,考虑到异步带来的不⼀致的问题,我们可以通过job去重试保证接⼝调⽤成功,⽽且⼀般公
司都会有核对的平台,⽐如下单成功但是未扣减积分的这种问题可以通过核对作为兜底的处理⽅案。

​编辑

使⽤mq之后我们的链路变简单了,同时异步发送消息我们的整个系统的抗压能⼒也上升了。

那你们使⽤什么mq?基于什么做的选型?

我们主要调研了⼏个主流的mq,kafka、rabbitmq、rocketmq、activemq,选型我们主要基于以下⼏个点去考虑:
1. 由于我们系统的qps压⼒⽐较⼤,所以性能是⾸要考虑的要素。
2. 开发语⾔,由于我们的开发语⾔是java,主要是为了⽅便⼆次开发。
3. 对于⾼并发的业务场景是必须的,所以需要⽀持分布式架构的设计。
4. 功能全⾯,由于不同的业务场景,可能会⽤到顺序消息、事务消息等。
基于以上⼏个考虑,我们最终选择了RocketMQ。

​编辑

你上⾯提到异步发送,那消息可靠性怎么保证?

消息丢失可能发⽣在⽣产者发送消息、MQ本身丢失消息、消费者丢失消息3个⽅⾯。

⽣产者丢失

⽣产者丢失消息的可能点在于程序发送失败抛异常了没有重试处理,或者发送的过程成功但是过程中⽹络闪断MQ没收到,消息就丢失了。
由于同步发送的⼀般不会出现这样使⽤⽅式,所以我们就不考虑同步发送的问题,我们基于异步发送的场景来说。
异步发送分为两个⽅式:异步有回调和异步⽆回调,⽆回调的⽅式,⽣产者发送完后不管结果可能就会造成消息丢失,⽽通过异步发送+回调通知+本地消息表的形式我们就可以做出⼀个解决⽅案。以下单的
场景举例。
1. 下单后先保存本地数据和MQ消息表,这时候消息的状态是发送中,如果本地事务失败,那么下单失败,事务回滚。
2. 下单成功,直接返回客户端成功,异步发送MQ消息
3. MQ回调通知消息发送结果,对应更新数据库MQ发送状态
4. JOB轮询超过⼀定时间(时间根据业务配置)还未发送成功的消息去重试
5. 在监控平台配置或者JOB程序处理超过⼀定次数⼀直发送不成功的消息,告警,⼈⼯介⼊。

​编辑

⼀般⽽⾔,对于⼤部分场景来说异步回调的形式就可以了,只有那种需要完全保证不能丢失消息的场景我们做⼀套完整的解决⽅案。

MQ丢失

如果⽣产者保证消息发送到MQ,⽽MQ收到消息后还在内存中,这时候宕机了⼜没来得及同步给从节点,就有可能导致消息丢失。
⽐如RocketMQ:
RocketMQ分为同步刷盘和异步刷盘两种⽅式,默认的是异步刷盘,就有可能导致消息还未刷到硬盘上就丢失了,可以通过设置为同步刷盘的⽅式来保证消息可靠性,这样即使MQ挂了,恢复的时候也可以从
磁盘中去恢复消息。
⽐如Kafka也可以通过配置做到:

​编辑

虽然我们可以通过配置的⽅式来达到MQ本身⾼可⽤的⽬的,但是都对性能有损耗,怎样配置需要根据业务做出权衡。

消费者丢失

消费者丢失消息的场景:消费者刚收到消息,此时服务器宕机,MQ认为消费者已经消费,不会重复发送消息,消息丢失。
RocketMQ默认是需要消费者回复ack确认,⽽kafka需要⼿动开启配置关闭⾃动offset。
消费⽅不返回ack确认,重发的机制根据MQ类型的不同发送时间间隔、次数都不尽相同,如果重试超过次数之后会进⼊死信队列,需要⼿⼯来处理了。(Kafka没有这些)

​编辑

你说到消费者消费失败的问题,那么如果⼀直消费失败导致消息积压怎么处理?

因为考虑到时消费者消费⼀直出错的问题,那么我们可以从以下⼏个⻆度来考虑:
1.消费者出错,肯定是程序或者其他问题导致的,如果容易修复,先把问题修复,让consumer恢复正常消费
2. 如果时间来不及处理很麻烦,做转发处理,写⼀个临时的consumer消费⽅案,先把消息消费,然后再转发到⼀个新的topic和MQ资源,这个新的topic的机器资源单独申请,要能承载住当前积压的
消息
3. 处理完积压数据后,修复consumer,去消费新的MQ和现有的MQ数据,新MQ消费完成后恢复原状

​编辑

那如果消息积压达到磁盘上限,消息被删除了怎么办?

这。。。TM都删除了我有啥办法啊。。。冷静,再想想。。有了。

​编辑

最初,我们发送的消息记录是落库保存了的,⽽转发发送的数据也保存了,那么我们就可以通过这部分数据来找到丢失的那部分数据,再单独跑个脚本重发就可以了。如果转发的程序没有落库,那就和消费⽅的记录去做对⽐,只是过程会更艰难⼀点。

说了这么多,那你说说RocketMQ实现原理吧?

RocketMQ由NameServer注册中⼼集群、Producer⽣产者集群、Consumer消费者集群和若⼲Broker(RocketMQ进程)组成,它的架构原理是这样的:
1. Broker在启动的时候去向所有的NameServer注册,并保持⻓连接,每30s发送⼀次⼼跳
2. Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择⼀台服务器来发送消息
3. Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费

​编辑

为什么RocketMQ不使⽤Zookeeper作为注册中⼼呢?

我认为有以下⼏个点是不使⽤zookeeper的原因:
1.根据CAP理论,同时最多只能满⾜两个点,⽽zookeeper满⾜的是CP,也就是说zookeeper并不能保证服务的可⽤性,zookeeper在进⾏选举的时候,整个选举的时间太⻓,期间整个集群都处于不
可⽤的状态,⽽这对于⼀个注册中⼼来说肯定是不能接受的,作为服务发现来说就应该是为可⽤性
⽽设计。
2.基于性能的考虑,NameServer本身的实现⾮常轻量,⽽且可以通过增加机器的⽅式⽔平扩展,增加集群的抗压能⼒,⽽zookeeper的写是不可扩展的,⽽zookeeper要解决这个问题只能通过划分领域,划分多个zookeeper集群来解决,⾸先操作起来太复杂,其次这样还是⼜违反了CAP中的A
的设计,导致服务之间是不连通的。
3.持久化的机制来带的问题,ZooKeeper的ZAB协议对每⼀个写请求,会在每个ZooKeeper节点上保持写⼀个事务⽇志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的⼀致性
和持久性,⽽对于⼀个简单的服务发现的场景来说,这其实没有太⼤的必要,这个实现⽅案太重
了。⽽且本身存储的数据应该是⾼度定制化的。
4.消息发送应该弱依赖注册中⼼,⽽RocketMQ的设计理念也正是基于此,⽣产者在第⼀次发送消息的时候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可⽤,短时
间内对于⽣产者和消费者并不会产⽣太⼤影响。

那Broker是怎么保存数据的呢?

RocketMQ主要的存储⽂件包括commitlog⽂件、consumequeue⽂件、indexfile⽂件。
Broker在收到消息之后,会把消息保存到commitlog的⽂件当中,⽽同时在分布式的存储当中,每个broker都会保存⼀部分topic的数据,同时,每个topic对应的messagequeue下都会⽣成consumequeue⽂件⽤于保存commitlog的物理位置偏移量offset,indexfile中会保存key和offset的对
应关系。

​编辑

CommitLog⽂件保存于${Rocket_Home}/store/commitlog⽬录中,从图中我们可以明显看出来⽂件名的偏移量,每个⽂件默认1G,写满后⾃动⽣成⼀个新的⽂件。​编辑

由于同⼀个topic的消息并不是连续的存储在commitlog中,消费者如果直接从commitlog获取消息效率⾮常低,所以通过consumequeue保存commitlog中消息的偏移量的物理地址,这样消费者在消费的时候先从consumequeue中根据偏移量定位到具体的commitlog物理⽂件,然后根据⼀定的规则(offset和⽂件⼤⼩取模)在commitlog中快速定位。​编辑

Master和Slave之间是怎么同步数据的呢?

⽽消息在master和slave之间的同步是根据raft协议来进⾏的:
1. 在broker收到消息后,会被标记为uncommitted状态
2. 然后会把消息发送给所有的slave
3. slave在收到消息之后返回ack响应给master
4. master在收到超过半数的ack之后,把消息标记为committed
5. 发送committed消息给所有slave,slave也修改状态为committed

你知道RocketMQ为什么速度快吗?

是因为使⽤了顺序存储、Page Cache和异步刷盘。
我们在写⼊commitlog的时候是顺序写⼊的,这样⽐随机写⼊的性能就会提⾼很多
写⼊commitlog的时候并不是直接写⼊磁盘,⽽是先写⼊操作系统的PageCache
最后由操作系统异步将缓存中的数据刷到磁盘

什么是事务、半事务消息?怎么实现的?

事务消息就是MQ提供的类似XA的分布式事务能⼒,通过事务消息可以达到分布式事务的最终⼀致性。半事务消息就是MQ收到了⽣产者的消息,但是没有收到⼆次确认,不能投递的消息。
实现原理如下:
1. ⽣产者先发送⼀条半事务消息到MQ
2. MQ收到消息后返回ack确认
3. ⽣产者开始执⾏本地事务
4. 如果事务执⾏成功发送commit到MQ,失败发送rollback
5. 如果MQ⻓时间未收到⽣产者的⼆次确认commit或者rollback,MQ对⽣产者发起消息回查
6. ⽣产者查询事务执⾏最终状态
7. 根据查询事务状态再次提交⼆次确认
最终,如果MQ收到⼆次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存下来并且在3天后被删除。

​编辑