消息中间件面试题(网上资料整理)

参考:消息中间件面试题:消息队列的优缺点,区别
参考:消息中间件面试题:消息丢失怎么办?
参考:RabbitMQ和Kafka的高可用
参考:中华石杉的架构
参考:阿里Java面试题剖析:如何解决消息队列的延时以及过期失效问题?

1.为什么使用消息队列

消息队列常见的使用场景吧,其实场景有很多,但是比较核心的有 3 个:解耦、异步、削峰。

1.解耦

A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃…
在这里插入图片描述

在这个场景中,A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。A 系统要时时刻刻考虑 BCDE 四个系统如果挂了该咋办?要不要重发,要不要把消息存起来?头发都白了啊!

如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。

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

2.异步

再来看一个场景,A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 1s,这几乎是不可接受的。
在这里插入图片描述
一般互联网类的企业,对于用户直接的操作,一般要求是每个请求都必须在 200 ms 以内完成,对用户几乎是无感知的。

如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了,爽!网站做得真好,真快!

在这里插入图片描述

3.削峰

一般的 MySQL,扛到每秒 2k 个请求就差不多了,如果每秒请求到 5k 的话,可能就直接把 MySQL 给打死了,导致系统崩溃,用户也就没法再使用系统了。

如果使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。而 MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就导致在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。

这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是 A 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。

在这里插入图片描述

2.消息队列有什么缺点

1.系统可用性降低

系统引入的外部依赖越多,越容易挂掉。

2.系统复杂度提高

硬生生加个 MQ 进来,你怎么[保证消息没有重复消费]?怎么[处理消息丢失的情况]?怎么保证消息传递的顺序性?

3.一致性问题

A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C
系统写库失败了,咋整?你这数据就不一致了。

3.RabbitMQ的高可用

RabbitMQ是基于主从复制来实现高可用的,不支持分布式。既然是主从复制,那么肯定就不是单台机器能保证的,所以是通过集群来保证高可用。而RabbitMQ的集群模式有两种:普通集群和镜像集群。真正实现高可用的是镜像集群模式。

1.普通集群模式

在这里插入图片描述
该集群模式不能保证高可用,它的作用只是提高了系统的吞吐量,同一个队列的消息,可以有多个消费者去消费。而且它的缺点是:在实例之间会产生网络传输,增加系统开销。

而且如果那个存有queue的真实数据的实例宕机了,会导致接下来其他实例都无法拉取数据;如果没有开启消息的持久化会丢失消息;就算开启了消息的持久化,消息不一定会丢,但是也要等这个实例恢复了,才可以继续拉取数据。

2.镜像集群模式

在这里插入图片描述
这种集群模式是可以保证可用的,因为每个实例都存有完整的数据,就算其中的某一个实例宕机了,也只是这一个实例不能提供服务,其他的实例都能继续提供服务。

缺点

  • 性能消耗太大,所有机器都要进行消息的同步,导致网络压力和消耗很大。
  • 没有扩展性可言,如果有一个queue负载很重,就算加了机器,新增的机器还是包含了这个queue的所有数据,并没有办法扩展queue。

实际上RabbitMQ并不是分布式消息队列,他就是传统的消息队列,只不过提供了一些集群、HA的机制而已,因为无论如何配置,rabbitmq一个queue的数据就存放在一个节点里面,镜像集群下,也是每个节点都放这个queue的全部数据。

3 开启镜像集群

在控制台新增一个镜像集群模式的策略,指定的时候可以要求数据同步到所有节点,也可以要求同步到指定节点,然后在创建queue的时候,应用这个策略,就会自动将数据同步到其他的节点上面去了。

4.Kafka的高可用

1.三大组件

Broker
Kafka集群包含一个或多个服务器,这种服务器被称为broker [5]
Topic
每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)
Partition
Partition是物理上的概念,每个Topic包含一个或多个Partition.

在这里插入图片描述

2.架构组成

Kafka的一个基本架构:多个broker组成,一个broker是一个节点;一个topic可以划分成多个partition,每个partition可以存在于不同的broker上面,每个partition存放一部分数据。这是天然的分布式消息队列。

Kafka在0.8以前是没有HA机制的,也就是说任何一个broker宕机了,那个broker上的partition就丢了,没法读也没法写,没有什么高可用可言。

Kafka在0.8之后,提过了HA机制,也就是replica副本机制。每个partition的数据都会同步到其他机器上,形成自己的replica副本。然后所有的replica副本会选举一个leader出来,那么生产者消费者都和这个leader打交道,其他的replica就是follower。写的时候,leader会把数据同步到所有follower上面去,读的时候直接从leader上面读取即可。

为什么只能读写leader
因为要是你可以随意去读写每个follower,那么就要关心数据一致性问题,系统复杂度太高,容易出问题。kafka会均匀度讲一个partition的所有数据replica分布在不同的机器上,这样就可以提高容错性。
这样就是高可用了,因为如果某个broker宕机 了,没事儿,那个broker的partition在其他机器上有副本,如果这上面有某个partition的leader,那么此时会重新选举出一个新的leader出来,继续读写这个新的leader即可。

写消息
写数据的时候,生产者就写向leader,然后leader将数据落到磁盘上之后,接着其他follower自己主动从leader来pull数据。一旦所有follower同步好了数据,就会发送ack给leader,leader收到了所有的follower的ack之后,就会返回写成功的消息给消息生产者。(这只是一种模式,可以调整)。
读数据
消费数据的时候,只会从leader进行消费。但是只有一个消息已经被所有follower都同步成功返回ack的时候,这个消息才会被消费者读到

5.消息丢失怎么办?

1.RabbitMQ的可靠性传输

在这里插入图片描述

1.生产者没有成功把消息发送到MQ

丢失的原因
因为网络传输的不稳定性,当生产者在向MQ发送消息的过程中,MQ没有成功接收到消息,但是生产者却以为MQ成功接收到了消息,不会再次重复发送该消息,从而导致消息的丢失。

事务机制解决
RabbitMQ 提供了事务功能,生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。

// 开启事务
	channel.txSelect
	try {
	    // 这里发送消息
	} catch (Exception e) {
	    channel.txRollback
	 
	    // 这里再次重发这条消息
	}
	// 提交事务
	channel.txCommit

confirm机制
RabbitMQ可以开启 confirm 模式,在生产者那里设置开启 confirm 模式之后,生产者每次写的消息都会分配一个唯一的 id,如果消息成功写入 RabbitMQ 中,RabbitMQ 会给生产者回传一个 ack 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 nack 接口,告诉你这个消息接收失败,生产者可以发送。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么可以重发。

注意
RabbitMQ的事务机制是同步的,很耗型能,会降低RabbitMQ的吞吐量。confirm机制是异步的,生成者发送完一个消息之后,不需要等待RabbitMQ的回调,就可以发送下一个消息,当RabbitMQ成功接收到消息之后会自动异步的回调生产者的一个接口返回成功与否的消息。

2.RabbitMQ接收到消息之后丢失了消息

丢失的原因
RabbitMQ接收到生产者发送过来的消息,是存在内存中的,如果没有被消费完,此时RabbitMQ宕机了,那么再次启动的时候,原来内存中的那些消息都丢失了。

持久化解决
开启RabbitMQ的持久化。当生产者把消息成功写入RabbitMQ之后,RabbitMQ就把消息持久化到磁盘。结合上面的说到的confirm机制,只有当消息成功持久化磁盘之后,才会回调生产者的接口返回ack消息,否则都算失败,生产者会重新发送。存入磁盘的消息不会丢失,就算RabbitMQ挂掉了,重启之后,他会读取磁盘中的消息,不会导致消息的丢失。
持久化设置

  • 第一点是创建 queue 的时候将其设置为持久化,这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
  • 第二个是发送消息的时候将消息的 deliveryMode 设置为 2,就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。

注意
持久化要起作用必须同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。

3. 消费者弄丢了消息

丢失的原因
如果RabbitMQ成功的把消息发送给了消费者,那么RabbitMQ的ack机制会自动的返回成功,表明发送消息成功,下次就不会发送这个消息。但如果就在此时,消费者还没处理完该消息,然后宕机了,那么这个消息就丢失了。

解决的办法
必须关闭 RabbitMQ 的自动 ack,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在编码中 ack 一下。这样的话,如果你还没处理完, RabbitMQ 就不会有 ack 。此时 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。(我的理解是让消费者消费完成之后返回处理结果,如果超时、失败表示失败,需要分发给其他的消费者执行)
在这里插入图片描述

2.Kafka的可靠性传输

在这里插入图片描述

1.生产者丢失消息

生产者把消息发送到了Kafka,但是Kafka还没保存到消息,就宕机了,导致了消息的丢失。但是如果我们为了不让Kafka丢失消息,进行了上面这样4个参数的配置,那么就不会出现生产者丢失消息了(见第二点)。因为消息一定要存到了副本上才会证明消息发送成功,否则生产者会一直重试。

2.Kafka丢失消息

丢失的原因
在Kafka的高可用集群环境中,生产者和消费者都是跟leader节点交流的,leader接收到生产者的消息之后,会同步到其他的follower中。如果leader在接收到消息之后,同步消息到follower的操作还没完成,此时leader就宕机了。那么就会从follower中选出一个新的leader,而这个leader中就会缺少没有同步的那些数据,消费者也就无法消费到,这就造成了消息的丢失。

解决的办法

  • 给 topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。
  • 在 Kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。
  • 在 producer 端设置 acks=all:这个是要求每条数据,必须是写入所有 replica 之后,才能认为是写成功了。
  • 在 producer 端设置 retries=MAX(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。

3.消费者端丢失消息

丢失的原因

唯一可能导致消费者弄丢数据的情况,就是说,你消费到了这个消息,然后消费者那边自动提交了 offset,让 Kafka 以为你已经消费了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢咯。

解决方案
Kafka 会自动提交 offset,那么只要关闭自动提交 offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。

在这里插入图片描述

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

业务场景是这样的:我们需要根据mysql的binlog日志同步一个数据库的数据到另一个库中,加如在binlog中对同一条数据做了insert,update,delete操作,我们往MQ顺序写入了insert,update,delete操作的三条消息,那么根据分析,最终同步到另一个库中,这条数据是被删除了的。但是,如果这三条消息不是按照insert,update,delete顺序被消费,而是按照delete,insert,update的顺序被消费,那么最终这条数据是会保存到新库中的。这就导致了数据错乱了。下面分别讲解下RabbitMQ和Kafka是如何保证消息的顺序性。

1.RabbitMQ出现消息顺序性错乱的情况

queue(队列)中的消息只能被一个消费者所消费,然后消费者在消费消息的过程中是无序的。如上图所示,如果按照BAC的消费顺序,那么最终数据库中是被保存这条数据的。这和我们预期的结果不符,如果这样的情况很多,那么就造成了数据库中的数据完成不对,同步工作也是白费了。
在这里插入图片描述

2.RabbitMQ保证消息顺序性

RabbitMQ保证消息的顺序性,就是拆分多个 queue,每个 queue 对应一个 consumer(消费者),就是多一些 queue 而已,确实是麻烦点;或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。

在这里插入图片描述

3.Kafka出现消息顺序性错乱的情况

首先要说明一些Kafka的特性:比如一个topic,有三个partition,生产者在写的时候可以指定一个key,假设以订单id作为key,那么这个订单key相关的数据,一定会被分发到同一个partition中去,并且是有序的。同时消费者从 partition 中取出来数据的时候,也一定是有顺序的。
。从Kafka的特性可以知道,这三条消息会分到一个partition中,一个消费者去消费这个partition中的数据,这个时候都能说是有序的。但是,单线程的情况下,吞吐量很低,一秒钟可能只能处理十多条消息,所以一般会采用多线程去提高吞吐量。线程之间的执行顺序和速度就不能保证有序了,如果thread1是拿到的delete,然后执行了,thread2拿到的消息是insert,然后执行,thread3拿到的消息是update,然后执行,所以最终这个订单的数据是会存到新库中的。这和原有的业务需求是不符合的。

在这里插入图片描述

4.Kafka保证消息的顺序性

每个线程都只从一个内存队列中取数据,都相关的数据都存到同一个内存队列中,就保证了数据的有序性。
在这里插入图片描述

7.如何保证消息幂等性

1.MQ出现非幂等性的情况

1.生成者重复发送消息给MQ

生成者把消息发送给MQ之后,MQ收到消息在给生产者返回ack的时候,网络中断了。这时MQ明明已经接收到了消息,但是生产者没接收到确定消息,就会认为MQ没有接收到消息。因此,在网络重新连接后,生产者会把已经发送的消息再次发送到MQ,如果MQ没有去重措施的话,那么就接收到了重复的消息。

2.MQ重复发送消息给消费者

消费者从MQ中拉取消息进行消费,当消费者已经消费了消息但还没向MQ返回ack的时候,消费者宕机或者网络断开了。所以消费者成功消费了消息的情况,MQ并不知道。当消费者重启或网络重连后,消费者再次去请求MQ拉取消息的时候,MQ会把已经消费的消息再次发送给消费者,如果消费者没有去重就直接消费,那么就会造成重复消费的情况。便会造成数据的不一致。

2. Kafka重复消费的场景

在这里插入图片描述

3.保证消息幂等性的办法

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

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

2.消费者不重复消费

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

  • 如果从MQ拿到数据是要存到数据库,那么可以根据数据创建唯一约束,这样的话,同样的数据从MQ发送过来之后,当插入数据库的时候,会报违反唯一约束,不会插入成功的。(或者可以先查一次,是否在数据库中已经保存了,如果能查到,那就直接丢弃就好了)。

  • 让生产者发送消息时,每条消息加一个全局的唯一id,然后消费时,将该id保存到redis里面。消费时先去redis里面查一下有么有,没有再消费。(其实原理跟第一点差不多)。

  • 如果拿到的数据是直接放到redis的set中的话,那就不用考虑了,因为set集合就是自动有去重的。

8.大量消息在 mq 里积压了几个小时了还没解决

一个消费者一秒是 1000 条,一秒 3 个消费者是 3000 条,一分钟就是 18 万条。所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概 1 小时的时间才能恢复过来。

一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:
先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。
新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。

9.mq 中的消息过期失效了

假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。

这个情况下,就不是说要增加 consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。

假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。

10.mq 都快写满了

如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。

11.如果让你写一个消息队列,该如何进行架构设计?

其实回答这类问题,说白了,不求你看过那技术的源码,起码你要大概知道那个技术的基本原理、核心组成部分、基本架构构成,然后参照一些开源的技术把一个系统设计出来的思路说一下就好。

比如说这个消息队列系统,我们从以下几个角度来考虑一下:

  • 首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?

  • 其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。

  • 其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。

  • 能不能支持数据 0 丢失啊?可以的,参考我们之前说的那个 kafka 数据零丢失方案。

mq 肯定是很复杂的,面试官问你这个问题,其实是个开放题,他就是看看你有没有从架构角度整体构思和设计的思维以及能力。确实这个问题可以刷掉一大批人,因为大部分人平时不思考这些东西。

发布了82 篇原创文章 · 获赞 15 · 访问量 3119

猜你喜欢

转载自blog.csdn.net/qq_34326321/article/details/103448736