直击灵魂的面试之MQ七连问

IT知识
207
0
0
2024-01-27
面试技巧:你需要去考虑一下你负责的系统中是否有本篇文章类似的场景,尽量去贴近实战去讲解

为什么要用MQ(MQ的优点)

首先要明白面试官问你这个问题主要是考察的点有哪些:
  • 你们公司有个什么场景(需要用到MQ)
  • 这个场景有个什么样的技术挑战(如果不用MQ可能会很麻烦),然后开始将MQ带来的好处

解耦

image.png

  • 解耦:一个系统A产生的数据,其他系统如果需要该数据,自己消费即可,不需要A系统去各个系统去做交互,也不用考虑其他系统服务出现问题做一些重试的处理、失败超时等问题。如果某个系统不需要该数据,则取消订阅即可,这样A系统就可以不需要过度依赖其他系统。其他系统也不会影响A系统。代码维护成本也会降低。

总结:通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。

异步

image.png

  • 异步:A系统/模块同步调用各个相关系统/模块,相对来说同步调用花费时间较长,因为同步调用需要各个依赖系统/模块逐一完成接口调用才能结束。这样耗时较长,用户无感知的时间大概是200ms。如果利用MQ将A系统/模块调用各个系统的API通过MQ队列进行异步化,这样可以大幅缩减调用时间,提高高延时接口的速度。

削峰

image.png

  • 削峰:在没有MQ的情况下系统A,在高峰期大量请求会涌入系统中,如果系统直接接入的MySQL,这样MySQL会直接崩掉。MySQL如果崩掉整个系统就会崩掉。这是在高峰期的情况也就是峰值,若是平常低谷时期整个系统是没有问题的。如果使用了MQ可以在峰值的时候,将所有请求都写入到MQ,假设系统A每秒钟最多只能处理两千请求,我们就可以在高峰期的时候每秒每次从MQ中最多拉取两千个请求,这样就能避免系统崩掉。但是这样会导致大量请求积压在MQ中,但是其实是可以接受。只要高峰期一过,在低谷时完全可以将积压的数据消费掉。

消息队列的缺点

系统引入MQ会引发什么样的问题?

  • 可用性降低:MQ一旦发生问题,就会导致生产者无法发送消息,其它消费者系统就无法消费消息 。导致整个系统就挂了
  • 复杂性变高:生产者系统/模块同样的数据生成两条,发送了两次(消息幂等性);生产者系统/模块给MQ发送的消息还没有被消费者系统/模块消费,数据就丢了(可靠性);生产者系统/模块发送给MQ的消息是顺序发送的,结果MQ导致顺序错乱,从而无法保证消费者系统/模块消费数据的顺序性(顺序性);消费者挂了导致MQ积压大量的数据。
  • 一致性的问题:各个系统/模块执行的结果不一致,有的成功,有的失败,但是用户得到的是成功。其实最终的正确的结果应该是失败的。

各个MQ的优缺点

特性

ActiveMQ

RabbitMQ

RocketMQ

Kafka

单机吞吐量

万级,比 RocketMQ、Kafka 低一个数量级

同 ActiveMQ

10 万级,支撑高吞吐

10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景

topic 数量对吞吐量的影响



topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic

topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源

时效性

ms 级

微秒级,这是 RabbitMQ 的一大特点,延迟最低

ms 级

延迟在 ms 级以内

可用性

高,基于主从架构实现高可用

同 ActiveMQ

非常高,分布式架构

非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用

消息可靠性

有较低的概率丢失数据

基本不丢

经过参数优化配置,可以做到 0 丢失

同 RocketMQ

功能支持

MQ 领域的功能极其完备

基于 erlang 开发,并发能力很强,性能极好,延时很低

MQ 功能较为完善,还是分布式的,扩展性好

功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用

综上,各种对比之后,有如下建议:

一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了;

后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高;

不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。

所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。

如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。

引入消息队列之后应该如何保证其高可用性

Rabbit MQ的高可用性

Rabbit MQ有三种模式:
  • 单机模式-(demo级别的,生产很少使用)
  • 普通集群模式
  • 镜像集群模式
普通集群模式(非分布式非高可用):

意思就是在多个机器上启动多个Rabbit MQ实例,每个机器启动一个,但是你创建的queue只会放在一个Rabbit MQ实例上,但是每个实例都去同步queue的元数据。这样在你消费的时候实际上如果连接到了另外一个实例,那么这个实例会去queue所在的实例上将数据拉取过来。这种方式很麻烦并且没有做到所谓的分布式,就是普通的集群。这样会导致要么消费者每次随机连接一个实例然后拉取数据要么就固定连接那个queue的实例消费数据,前者有数据拉取的开销,后者导致单实例的瓶颈。

  • 优点:
  1. 这种模式只是提高了消费者消费的吞吐量
  • 缺点:2. 可能会在Rabbit MQ集群内部产生大量的数据传输3. 可用性没有什么保障,如果queue所在的节点宕机了,数据就丢失了,因为那个queue所在的实例包含元数据和实际数据
镜像集群模式(非分布式高可用):

这种模式才是所谓的Rabbit MQ真正的高可用模式,与普通集群模式不同的是:你创建的queue无论是元数据还是queue的消息会存在于多个实例上,每次写消息到queue的时候,都会自动把消息与多个实例的queue进行消息同步。

  • 优点:
  1. 其中某个机器宕机了,别的机器还可以继续提供服务。
  • 缺点:
  1. 这个性能相比较而言开销较大,消息需要同步所有消息。导致网络带宽压力和消耗很重。
  2. 还有一点就是所谓的扩展性几乎没有,因为假设某个queue的数据负载很重,加机器无法线性去扩展queue。
如何开启Rabbit MQ的镜像模式

在管理控制台新增一个镜像集群的策略,要求所有节点同步数据。

Kafka的高可用性

  • 基本认识:

Kafka是多个broker组成,每个broker是一个节点,你创建一个topic,这个topic可以划分为多个partition,每个partition可以存在不同的broker上,每个partition就放一部分数据。天然的分布式消息队列。因为一个topic数据是分散在多个机器上的。每个机器之存放一部分数据。Tip:

Kafka0.8之前是没有HA(高可用)机制的。就是任何一个broker宕机了,那么这个broker上的partition就废了,没法写也没有办法读。Kafka0.8以后,提供了HA机制,就是replica副本机制,每个partition的数据都会同步到其它机器上,形成自己的多个replica副本。然后所有replica会选举出一个leader出来,那么生产和消费都和这个leader打交道,然后其它的replica就是follower。写的时候leader只负责把数据同步到follwer上,读的时候直接读leader。如果leader宕机follwer会变成leader,follwer变成leader过程无法提供对外服务。

如何保证消息不被重复消费?(如何保证消息消费时的幂等性?)

Kafka(消费者offset没来得及提交导致重复消费)

  • 生成者不重复发送消息到MQ

mq内部可以为每条消息生成一个全局唯一、与业务无关的消息id,当mq接收到消息时,会先根据该id判断消息是否重复发送,mq再决定是否接收该消息。

  • 消费者不重复消费

消费者怎么保证不重复消费的关键在于消费者端做控制,因为MQ不能保证不重复发送消息,所以应该在消费者端控制:即使MQ重复发送了消息,消费者拿到了消息之后,要判断是否已经消费过,如果已经消费,直接丢弃。所以根据实际业务情况,有下面几种方式:

  • 如果从MQ拿到数据是要存到数据库,那么可以根据数据创建唯一约束,这样的话,同样的数据从MQ发送过来之后,当插入数据库的时候,会报违反唯一约束,不会插入成功的。(或者可以先查一次,是否在数据库中已经保存了,如果能查到,那就直接丢弃就好了)。
  • 让生产者发送消息时,每条消息加一个全局的唯一id,然后消费时,将该id保存到redis里面。消费时先去redis里面查一下有么有,没有再消费。(其实原理跟第一点差不多)。
  • 如果拿到的数据是直接放到redis的set中的话,那就不用考虑了,因为set集合就是自动有去重的。

如何保证消息的可靠性传输(如何处理消息丢失的问题?)

主要分为三种:

  1. 生产者丢失
  2. MQ自己丢失了
  3. 消费的时候丢了

Rabbit MQ消息丢失的情况及如何处理

image.png

生产者弄丢了消息
写消息的过程中,消息都没到Rabbit MQ,在网络传输过程中就丢了;或者是消息到了Rabbit MQ但是MQ内部错乱没有存下来导致消息丢失。
  • 方案1:可以使用Rabbit MQ事务机制如下:
channel.txSelect();
try{
    // 发送消息
}catch{
    channel.txRollback();
}
channel.txCommit();
弊端:是事务机制,同步阻塞的,会导致生产者发送消息的吞吐量大大下降。
  • 方案2:把channel设置成comfirm模式,发送一个消息就不用管了,Rabbit MQ如果接收到了这个消息就会回调生产者本地的一个接口,通知你说这条消息已经发送成功并且接收成功。反之也会通知。
该方式的吞吐量会高一些
Rabbit MQ本弄丢了消息

将Rabbit MQ设置成持久化的。除非有极其罕见的情况Rabbit MQ还没来得及持久化自己就挂了,可能回导致少量的数据丢失。这种概率很小。设置持久化的步骤(必须两个同时设置):

  1. 创建queue的时候将其设置为持久化的,这样保证Rabbit MQ持久化queue的元数据,但是不会持久化queue里的数据
  2. 发送消息的时候将消息的deliveryMode设置为2.就是将消息设置为持久化。此时Rabbit MQ就会将消息持久化到磁盘上去。
消费者弄丢了消息

只有当你打开了消费者的autoAck的这样一个机制;你消费到了数据之后消费者会自动通知Rabbit MQ说我已经消费到了这条数据;这样会出现一种情况就是假设消费到了一条数据还没处理完,此时消费者就自动autoAck了,此时恰巧消费者系统服务挂掉了,消息还没处理完而且Rabbit MQ以为该消息已经处理掉了。

  • 方案:关闭掉Rabbit MQ的自动ACK机制。

Kafka消息丢失的情况

消费者弄丢了消息

消费者自动提交了offset,其实消息还没有处理完。和Rabbit MQ情况差不多。

  • 解决方案:关闭自动提交offset,手动提交offset。
Kafka弄丢了消息

Kafka的leader接收到了消息但是还没来得及同步给follwer就挂掉了,此时follwer变成了leader,导致数据丢失了

  • 解决方案:设置4个参数
  • 给topic设置replication.replicas参数,这个值必须大于1,也就是要求每个partition至少有两个副本
  • 在Kafka服务端设置min.insync.replicas参数:这个值必须大于1,这个是要求一个leader至少感知到至少有一个follwer还跟自己保持联系,这样才能确保leader还有一个follwer。
  • 在producer端设置ack=all:这个是要求每条数据必须是写入所有的replica之后,才能认为是成功了
  • 在producer端设置retries=MAX:这个是要求一旦写入失败,就无限重试,卡在这里。
按照上述配置后至少在Kafka端就可以保证在leader所在的broker发生故障,进行leader切换时,数据不会丢失。
生产者会不会丢失数据

如果按照上述方式设置了ack=all一定不会丢,要求是:你的leader接收到消息,所有的follower都同步到了消息之后,才认为本次消息发送成功,否则生产者会重试无限次。

7. 如何保证消息的顺序性

假设做一个MySQL binlog同步系统,你在MySQL里增删改一个条数据,对应出来增删改3条binlog,接着将这三条binlog发送到MQ里面,到消费出来一次执行,需要保证消息的顺序性。不然数据就会出现问题。

不同MQ错乱的场景:

Rabbit MQ

  • Rabbit MQ:一个queue,多个consumer,这就会出现问题;因为多个消费者是同步一起执行的,无法保证顺序,并且也无法保证消费者消费到了哪条数据。

image.png

解决方案
  • 解决方案:每个消费者建立对应的queue,并且让保持顺序的消息只发送到一个queue上,这样消费者消费数据处理的时候就不会出现顺序错乱。

image.png

Kafka

Kafka可以保证生产者写入一个partition的数据一定是有顺序的。
如何保证数据写入一个partition中去:

生产者在写入数据的时候可以指定一个key,比如指定某个订单id作为key,这个订单相关的数据就会被分发到一个partition中去。Kafka有一个原则是一个partition只能被一个消费者消费消费者从partition中取出来数据的时候,一定是有顺序的。

什么情况下Kafka会出现消息顺序不一致呢?

消费者内部搞多个线程并发处理,则可能会出现顺序不一致的问题。

解决方案

按照hash算法进行hash分发。相同的订单key的数据分发到同一个内存queue里面。如图

如何解决消息延时过期失效的问题

Rabbit MQ有一个TTL过期时间。关掉不要开启TTL

如何解决消息积压的问题

Rabbit MQ消息积压

  • 解决思路:

临时紧急扩容。具体操作如下:

由于消费者故障的解决方案
  1. 如果consumer有问题,先修复consumer的问题,确保其恢复消费速度。然后将现有consumer都停掉。
  2. 临时建立好原先10倍或者20被queue的数量。(Kafka-新建一个topic,partition是原来的10倍。)
  3. 写一个临时分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时处理,直接均匀轮询写入已经建立好10倍/20倍数量的queue。
  4. 接着临时征用10倍的机器来部署consumer,每一批consumer来消费一个临时queue的数据。
  5. 等快速消费完积压的数据之后,得恢复原来部署架构,重新用原来的consumer机器来消费