MQ面试篇

167 阅读16分钟

为什么要用MQ

MQ的作用

MQ的作用主要有三个解耦,异步,削峰

解耦

如果多个模块或者系统中,互相调用很复杂,维护起来比较麻烦,但是这个调用又不是同步调用,就可以运用MQ到这个业务中。

异步

这个很好理解,比如用户的操作日志的维护,可以不用同步处理,节约响应时间。

削峰

在高峰期的时候,系统每秒的请求量达到 5000,那么调用 MySQL 的请求也是 5000,一般情况下MySQL 的请求大概在 2000 左右,那么在高峰期的时候,数据库就被打垮了,那系统就不可用了。此时引入MQ,在系统 A 前面加个 MQ,用户请求先到 MQ,系统 A 从 MQ 中每秒消费 2000 条数据,这样就把本来 5000 的请求变为 MySQL 可以接受的请求数量了,可以保证系统不挂掉,可以继续提供服务。MQ 里的数据可以慢慢的把它消费掉。

使用了MQ会有什么问题

  • 增加了系统的复杂度:因为一个系统引入了MQ之后会造成系统的复杂性的提升,复杂性提升后,增加MQ的维护成本
  • 降低的系统的可用性:复杂性的提升意味这系统可用性的降低,因为MQ一旦出现问题就会造成系统出现问题。
  • 一致性问题:因为MQ是异步处理消息,需要处理类似于消息丢失以及重复消费的问题,一旦处理不好就会造成重复消费问题。

如何避免MQ消息堆积

产生MQ消息堆积的原因

  • 生产者投递消息的速率与我们消费者消费的速率完全不匹配。
  • 生产者投递消息的速率>消费者消费的速率 导致我们消息会堆积在我们 mq 服务器端中,没有及时的被消费者消费 所以就会产生消息堆积的问题
  • 注意的是:rabbitmq 消费者我们的消息消费如果成功的话 消息会被立即删除。 kafka 或者rocketmq 消息消费如果成功的话,消息是不会立即被删除。

解决办法

  • 提高消费者消费的速率;(对我们的消费者实现集群)
  • 消费者应该批量形式获取消息 减少网络传输的次数;

为什么会出现重复消费?

MQ的消息流程主要有两个阶段来完成,发送消息到消息队列以及消息队列将消息投递到消费者,因为各种网络原因会造成发送方与以及接收方消息重试,就会造成重复消费

发送方消息重试

因为网络原因以及MQ自身原因会导致发送的消息未成功投递到MQ,如果出现未成功投递就会重复投递消息,这个时候是正常的,但是可能因为网络原因导致消息确认消息延时,实际上已经投递到了MQ但是确认消息没有及时被发送方接收到,就会重新发起消息重试,这样就会造成消息重复,这种是发送方消息重试,但是发送方重试次数是有限制的,如果达到一定次数后还是失败如果没有做其他处理就会造成消息丢失。

消费方消息重试

当消息正常投递到MQ后,就需要消费消息了,正常情况下消息会被发送到消费方进行消费,消费方一般需要开启手动确认来确定消息一定会被消费掉,但是可能网络原因导致消息未被消费,这个时候消费方就会重试消费消息,如果是出现消费超时或者异常就会进行消息重复消费,如果异常没有处理好导致消息重复消费可能造成业务出现幂等性问题,但是如果做了幂等性问题,多次消费还是失败就会造成消息丢失。

如果出现重复消费如何解决

唯一主键

手动给每一条消息增加一个唯一的消息ID,使用消息ID作为唯一主键,如果消息重复,消息插入不进去,用这种方式来解决消息重复

使用幂等

使用幂等方式来解决重复消费问题,手动给每一条消息增加一个唯一的消息ID,不要使用系统生成的消息ID,如果生成消息后就将消息保存到Redis中,如果消息消费成功后删除Redis中的消息,如果失败了不需要管REDIS中的消息,这样就完来实现消息的幂等。

image.png

如何保证消息顺序性

问题解析

一个queue,有多个consumer去消费,这样就会造成顺序的错误,consumer从MQ里面读取数据是有序的,但是每个consumer的执行时间是不固定的,无法保证先读到消息的consumer一定先完成操作,这样就会出现消息并没有按照顺序执行,造成数据顺序错误,并且执行的时候是多线程执行的,并不能保证执行的顺序性。

image.png

解决办法

image.png

如何实现延时消息

延时消息就是一个消息需要延时多长时间才能够发送,比如用户支付订单,如果多长时间未支付就会取消订单,这个就属于延时消息

延时消息实现方式

  • 数据库轮询

通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行update或delete等操作

  • JDK的延迟队列

image.png

利用JDK自带的DelayQueue来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入DelayQueue中的对象,是必须实现Delayed接口的。

  • netty时间轮算法

image.png

  • RabbitMQ

可以采用RabbitMQ的死信队列来实现延时队列,RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter,dead letter会被投递到死信交换器,然后通过死信队列将消息发送出去

image.png

  • RocketMQ

RocketMQ可以实现18个等级的消息延时,但是不可以实现任意时间的消息延时,使用RocketMQ的延时消息只需要按照正常消息发送,并指定延时等级即可,简单高效,并且这个延时时间可以在RocketMQ的配置参数中进行配置。

怎样选型MQ

需求分析

功能需求

除了最基本生产消费模型,还需要MQ能支持REQUEST-REPLY模型,以提供对同步调用的支持。 此外,如果MQ能提供PUBLISH-SUBSCRIBE模型,则事件代理的实现可以更加简单。

性能需求

考虑未来一到两年内产品的发展,消息队列的呑吐量预计不会超过 1W qps,但由单条消息延迟要求较高,希望尽量的短。

可用性需求

因为是在线服务,因此需要较高的可用性,但充许有少量消息丢失。

易用性需求

包括学习成本、初期的开发部署成本、日常的运维成本等。

横向对比

image.png

image.png

选型参考

image.png

业务为什么使用rocketmq 不用kafka

因为kafka的诞生是作为大数据的一个中间件来使用的,MQ只是kafka的一个功能,相对于 RocketMQ,Kafka的功能相对于简单,RocketMQ设计的时候借鉴了很多kafka的设计,有着后发优势,支持分布式、集群、副本等机制,消息延时也比较少,但是RocketMQ也有很多kafka不具备的功能,比如:严格顺序消息,延时消息,服务端tag过滤,能够至少保证消费一个的可靠性策略,消息失败后重试时间随着次数递增等都是很多业务所需要的,并且RocketMQ是经过了双十一的验证,所以很多人说RocketMQ是专门为业务而生的。

为什么kafka不能支持大量的topic

这句话如果严格意义上说的话应该是kafka在topic-partition过多而不是单纯的topic过多这个要从kafka的存储结构来说起,kafka的最小存储单元是一个partition,一个partition由一个数据文件以及一个索引文件组成,partition的存储文件是通过顺序写的方式来保证写入磁盘的效率的,但是如果非常多的partition需要磁盘写入,那么就可能造成顺序写变成随机写,因为磁盘的IO流量就那麽大,同时由很多个partition需要写入,就会变成随机写,性能反而会急剧下降,并且partition过多还是导致元数据管理,副本同步的困难,这些都是导致kafka不能支持大量topic的原因。

为什么RcoketMQ可以支持大量的Topic

image.png 这个需要从RocketMQ的存储结构来说,RocketMQ的所有的数据存储在一个commitlog,然后则会通过异步线程将commitlog的数据转移到其他对应的消费者的队列中,因为不管多少个队列也只有一个commitlog文件,所有写入以及读取性能不会随着队列的增加而有显著的变化

下面是RocketMQ和kafka的topic的测试报告

image.png

RabbitMQ交换器有哪些特点是什么

主要有以下4种

  • fanout:把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。
  • direct:把消息路由到BindingKey和RoutingKey完全匹配的队列中。
  • topic:Topic交换器能够根据路由键匹配根据规则匹配相关的交换器,主要的表达式有 # 和 *
  • headers:不依赖路由键匹配规则路由消息。是根据发送消息内容中的 headers 属性进行匹配。性能差,基本用不到。

Virtual Host是什么

每一个RabbitMQ服务器都能创建虚拟的消息服务器,也叫虚拟主机(virtual host),简称vhost。

RabbitMQ集群中的节点类型有哪些

集群模式有以下几种

  • 单机版RabbitMQ
  • 集群模式,普通集群、高可用集群
  • 镜像队列 节点类型有以下几种
  • 内存节点:ram,将变更写入内存。
  • 磁盘节点:disc,磁盘写入操作。
  • RabbitMQ要求最少有一个磁盘节点。

导致的死信的几种原因

  • 消息被拒( Basic.Reject /Basic.Nack ) 且 requeue = false 。
  • 消息TTL过期。
  • 队列满了,无法再添加。

RabbitMQ如何保证消息的可靠性

  • 生产者到RabbitMQ :失败通知,发布者确认,备用交换器。
  • RabbitMQ自身 :持久化、高可用集群模式,镜像模式。
  • RabbitMQ到消费者 :basicAck机制、死信队列、消息补偿机制。

什么是RocketMQ的事务消息

事务消息就是MQ提供的类似XA的分布式事务能力,通过事务消息可以达到分布式事务的最终一致性。半事务消息就是MQ收到了生产者的消息,但是没有收到二次确认,不能投递的消息。

实现原理

  • 生产者先发送一条半事务消息到MQ
  • MQ收到消息后返回ack确认
  • 生产者开始执行本地事务
  • 如果事务执行成功发送commit到MQ,失败发送rollback
  • 如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查
  • 生产者查询事务执行最终状态
  • 根据查询事务状态再次提交二次确认

最终,如果MQ收到二次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存下来并且在3天后被删除。

image.png

Broker是怎么保存数据的呢

image.png

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

RocketMQ如何保证消息的可靠性

生产者丢失

生产者可能因为网络原因,broker故障等原因丢失消息,使用同步发送不会出现这种问题,因为只要开启master的同步刷盘,异步复制,基本上能够保证消息不会丢失,如果消息不能落盘到master中,这消息就不会发送成功,并且发送端有发送方重试,如果消息发送失败会给出消息发送失败,我们在做处理就可以的。

MQ丢失

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

消费者丢失

消费者丢失消息的场景:消费者刚收到消息,此时服务器宕机,MQ认为消费者已经消费,不会重复发送消息,消息丢失。RocketMQ默认是需要消费者回复ack确认,消费方不返回ack确认,重发的机制根据MQ类型的不同发送时间间隔、次数都不尽相同,如果重试超过次数之后会进入死信队列,需要手工来处理了,消息在死信队列的时间一般在72小时,如果不及时处理可能造成消息丢失。

RocketMQ如何保证的可靠性的

RocketMQ在设计之初就保证了消息至少被消费一次,我们只需要保证消息能够正常投递到MQ中就可以的

发送方保证消息的可靠性

发送的的时候有三种方式,同步,异步,单向,其中同步异步都有发送发确认机制,如果发送失败会进行重试,而单向发送则只管发送不管结果

RocketMQ保证消息可靠性

image.png

  • RocketMQ由nameserver和broker组成,nameserver之间是不互相通讯的,这样当一个
  • nameserver挂掉后会找到另一个nameserver重试消息
  • RocketMQ的broker是一个主从架构,可以配置一主一从或者多主多从,MQ的主节点收到消息后通过同步策略会将消息复制到从节点中。
  • RocketMQ的消息同步策略有同步复制和异步复制,而如果要保证消息不丢失可以选择同步复制,只有消息同步复制到了从节点后才会确认消息发送到MQ成功
  • Broker 故障规避,默认情况下延迟规避策略只在重试时生效,例如在一次消息发送过程中如果遇到消息发送失败,规避 broekr-a,但是在下一次消息发送时,即再次调用DefaultMQProducer的 send 方法发送消息时,还是会选择 broker-a 的消息进行发送,只要继续发送失败后,重试时再次规避 broker-a。
  • RocketMQ的存储方式有两种同步刷盘以及异步刷盘,同步刷盘是指消息必须写入磁盘后才会成功,而异步刷盘是指消息必须只要写入内存就是成功,但是在还没有写入磁盘的过程中如果断电可能造成部分消息丢失

消费端保证消息不丢失

image.png

  • RocketMQ为了保证消费进度不丢失使用了只记录最小的消费进度,比如消费端消费了100-200之间的消息,如果第101条消息没有消费成功101后面的所以消息消费成功,如果这个时候消费者重启也会导致消息从101-200之间的所有消息进行重试
  • 为了防止消息一直重试的问题,引入了死信队列,当消息一直消费不了的时候就会进入死信队列交给死信队列处理
  • 如果消费成功后的消息因为删库跑步导致数据丢失,还可以通过RocketMQ的消息回溯功能将以前消费过的消息再消费一遍

为什么RocketMQ不使用Zookeeper作为注册中心呢

  • 根据CAP理论,同时最多只能满足两个点,而zookeeper满足的是CP,也就是说zookeeper并不能保证服务的可用性,zookeeper在进行选举的时候,整个选举的时间太长,期间整个集群都处于不可用的状态,而这对于一个注册中心来说肯定是不能接受的,作为服务发现来说就应该是为可用性而设计
  • 基于性能的考虑,NameServer本身的实现非常轻量,而且可以通过增加机器的方式水平扩展,增加集群的抗压能力,而zookeeper的写是不可扩展的,而zookeeper要解决这个问题只能通过划分领域,划分多个zookeeper集群来解决,首先操作起来太复杂,其次这样还是又违反了CAP中的A的设计,导致服务之间是不连通的。
  • 持久化的机制来带的问题,ZooKeeper 的 ZAB 协议对每一个写请求,会在每个ZooKeeper 节点上保持写一个事务日志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的一致性和持久性,而对于一个简单的服务发现的场景来说,这其实没有太大的必要,这个实现方案太重了。而且本身存储的数据应该是高度定制化的。
  • 消息发送应该弱依赖注册中心,而RocketMQ的设计理念也正是基于此,生产者在第一次发送消息的时候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可用,短时间内对于生产者和消费者并不会产生太大影响。