消息队列面经

1 消息队列的使用场景?

  1. 异步:A系统需要发送请求给B系统处理,由于B系统需要查询更新数据库花费时间较长,以至于A系统要等待B系统处理完毕后再发送下个请求,造成A系统资源浪费。使用消息队列后,A系统生产完消息后直接丢进消息消息队列,就完成一次请求,继续处理下个请求。
  2. 解耦:A系统发送个数据到BCD三个系统,接口调用发送,那如果E系统也要这个数据呢?那如果C系统现在不需要了呢?现在A系统又要发送第二种数据了呢?更加崩溃的是,A系统要时时刻刻考虑BCDE四个系统如果挂了咋办?我要不要重发?我要不要把消息存起来?使用消息队列就能解决这个问题,A系统只负责生产数据,不需要考虑消息被哪个系统来消费。
  3. 削峰:A系统调用B系统处理数据,每天0点到11点,A系统风平浪静,每秒并发请求数量就100个。结果每次一到11点~1点,每秒并发请求数量突然会暴增到1万条。但是B系统最大的处理能力就只能是每秒钟处理1000个请求啊,那系统就会直接崩掉。引入消息队列后,把请求数据先存入消息中间件系统中,消费系统慢慢拉取消费,可能会比正常的慢一点,但是不至于打挂服务器。等流量高峰下去了,服务也就没压力了。

2 消息队列带来的问题?

  1. 系统复杂性提高:本来代码想怎样写就怎样写,现在加入一个 MQ 之后,还得考虑如何去维护它。除此之外,还有一大堆问题,例如消息重复消费、消息丢失、消息的顺序消费等等,烦死了。
  2. 数据一致性问题:A系统处理完了直接返回成功了,别人以为你这个请求就成功了。但是问题是,要是BCD三个系统那里,BD两个系统写库成功了,结果C系统写库失败了,怎么办?这数据就不一致了。(解决方法:分布式事务)
  3. 系统可用性降低:系统引入的外部依赖越多,越容易挂掉,本来就是A系统调用BCD三个系统的接口就好了,ABCD四个系统好好的,没啥问题,偏偏加个 MQ 进来,万一 MQ 挂了怎么办呢?MQ 挂了,整套系统崩溃了,这不就凉了吗?

3 消息队列技术选型?

在这里插入图片描述

4 如何保证消息不被重复消费?

我们可以使用接口幂等来处理消息重复消费的问题。

幂等是一个数学与计算机学概念,在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。例如,setTrue() 函数就是一个幂等函数,无论多次执行,其结果都是一样的,更复杂的操作幂等保证是利用唯一交易号(流水号)来实现的。

那么是如何保证幂等的呢?一般幂等,需要分场景去考虑,看是强校验还是弱校验,比如跟金钱相关的场景就很关键,要做强校验,别不是很重要的场景做弱校验。

以电商举例,强校验就是当有消息过来时拿着订单号+业务场景这样的唯一标识去流水表查,看看有没有这条流水,有就直接 return 不要走下面的流程了,没有就执行后面的逻辑。弱校验就是使用 id +场景唯一标识作为 Redis 的 key,放到缓存里面,失效时间看场景,一定时间内的这个消息就去 Redis 判断,这样有一定概率会发生消息丢失,但在这样的场景下也是可以接受的。

5 如何保证消息的顺序性?

生产者消费者一般需要保证顺序消息的话,可能就是一个业务场景下的,其 id 应该是一致的,例如订单的创建、支付、发货、收货,它们的订单号总得是相同的吧?

既然如此,我们让同一个订单发送到同一个队列中,再使用同步发送,只有同个订单的创建消息发送成功,再发送支付消息。这样,我们保证了发送有序。需要注意的是,消息队列仅保证顺序发送,顺序消费由消费者业务保证。

6 如何处理消息丢失的问题?

我们以 RabbitMQ 为例子。

数据在生产者中丢失

可能数据在从生产者到消息队列的路上就丢失了,原因可能有很多,比如网络问题啥的。我们可以使用 RabbitMQ 的事务功能,生产者发送数据之前开启 RabbitMQ 事务,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务,然后重试发送消息;如果收到了消息,那么可以提交事务。但这有个大问题,使用事务机制之后,吞吐量会降低,造成性能下降比较严重。

事实上我们可以使用 confirm 模式。这样每次写消息都会分配一个唯一的 id,如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 ack 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你一个 nack 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。

事务机制和 cnofirm 机制最大的区别在于,事务机制是同步的,提交完事务会阻塞,而 confirm 机制是异步的,发送完一条消息之后就可以继续发送下一条消息,当 RabbitMQ 接收到消息之后会异步回调一个接口通知你这个消息接收到了。一般来说,都选择使用 cnofirm 机制。

数据在 RabbitMQ 中丢失

持久化 RabbitMQ,即消息写入之后会持久化到磁盘即可。

设置 RabbitMQ 的持久化有两个步骤,一是创建 queue 时将其设置为持久化的,这样 RabbitMQ 会持久化 queue 的元数据,但是不会持久化 queue 里的数据。二是将消息设置为持久化的,这样 RabbitMQ 就会将消息持久化到磁盘上去。

持久化可以跟生产者的 confirm 机制配合,只有消息被持久化到磁盘之后,才会通知生产者 ack,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 ack,你也是可以自己重发的。

数据在消费者中丢失

使用 RabbitMQ 提供的 ack 机制,即关闭 RabbitMQ 的自动 ack,然后在代码中手动 ack。这样,如果消息丢失,就不会 ack,RabbitMQ 就会把这个消息分配给其他消费者去处理。

7 如何保证消息队列的高可用?

我们以 RabbitMQ 为例子讲解一下消息队列的高可用性是如何实现的。RabbitMQ 具有三种模式:

单机模式

在本地用来做 demo 的,正常来说生产环境没人敢用吧

普通集群模式

在多台机器上启动多个 RabbitMQ 实例,每台机器启动一个。但是创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据。消费的时候,如果连接到了另外一个 RabbitMQ 实例,那么那个实例会从 queue 所在实例上拉取数据过来。

因为这导致要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个 queue 所在实例消费数据,前者有数据拉取的开销,后者导致单实例性能瓶颈。而且如果那个放 queue 的实例宕机了,会导致接下来其他实例就无法从那个实例拉取。如果开启了消息持久化,让 RabbitMQ 落地存储消息的话,消息不一定会丢,得等这个实例恢复了,然后才可以继续从这个 queue 拉取数据。

这方案主要是用来提高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。

镜像集群模式

这种模式是所谓的 RabbitMQ 的高可用模式。在这种模式下,创建的 queue 无论是元数据还是 queue 里的消息都会存在于多个实例上,然后每次写消息到 queue 时,都会自动把消息到多个实例的 queue 里进行消息同步。

这种模式优点在于即使有机器宕机,其他机器也可以继续使用。但其缺点也非常明显,其一是性能开销大,消息同步所有机器,导致网络带宽压力和消耗很大。其二是该模式缺少扩展性,如果某个 queue 负载很重,即使加机器,新增的机器也包含了这个 queue 的所有数据,并没有办法线性扩展 queue。.

8 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时呢?

大量消息在 MQ 里积压了几个小时了还没解决?

一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:

  1. 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。
  2. 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
  3. 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
  4. 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
  5. 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。

MQ 中的消息过期失效了?

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

这个情况下,就不是说要增加 consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,就是大量积压的时候,就直接丢弃数据了,然后等过了高峰期以后,再开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 MQ 里面去,把白天丢的数据给他补回来。

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

MQ 都快写满了?

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

参考:《吊打面试官》系列-消息队列基础
消息队列
消息中间件面试题:如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时呢?

发布了123 篇原创文章 · 获赞 226 · 访问量 2万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章