面试 MQ
Kiml Lv5
  • 前言
    ❗表示必掌握,❔表示基本不会问

  • 更新

1
24-08-22 初始记录

你知道目前市面上使用的 MQ 有哪些吗?

RabbitMQ、RocketMQ、Kafka。

你使用的是哪种 MQ?为什么选择这类 MQ(qps)?

我们选用的是 RabbitMQ,它并发能力强,性能好,延时低,管理界面也很丰富,只是吞吐量较低,但对于不是特别依赖大数据的项目来说,选用 RabbitMQ 已经足够用了。

RocketMQ 是阿里开源项目,吞吐量是最高的(10 万次级),但免费版的 MQ 是阉割版的,容易出问题。

Kafka 吞吐量很大(10 万次级),但它不是真正的 MQ,只是类似 MQ 的产品,它只支持主要 MQ 功能,比如它不具有消息确认机制。

什么是 RabbitMQ?

RabbitMQ 是一款开源的,Erlang 编写的,基于 AMQP 协议的消息中间件。

什么是 AMQP 协议?

AMQP 一个提供统一消息服务的应用层标准高级消息队列的链接协议,RabbitMQ 是主要根据 AMQP 协议进行数据通信和传输的。有点类似于 HTTP 协议。

AMPQ 与 JMS 有什么区别知道吗?

JMS 是定义了统一的接口(API),来对消息操作进行统一;AMQP 是通过规定协议来统一数据交互的格式。

JMS 限定了必须使用 Java 语言;AMQP 只是协议,不规定实现方式,因此是跨语言的。

JMS 规定了两种消息模式;而 AMQP 的消息模式更加丰富。

你使用的是 SpringCloud,Feign 可以进行远程调用,为什么还要中间加一个 MQ 呢?

为了解耦,如果没有中间件进行处理,那两个系统之间的关系过于紧密,一方改动,另一方也必须改动。

MQ 有哪些优势?

  1. 应用解耦(核心):降低系统的耦合性,提升可维护性。
    场景:服务调用之间都可以考虑 MQ。

  2. 异步提速:提升用户体验和系统吞吐量。
    场景:发送订单消息、发送短信消息等。

  3. 削峰填谷:减少高峰时期对服务器的压力。
    场景:秒杀活动、限时定购等。

RabbitMQ 有什么缺点?

  1. 系统可用性降低
    本来系统运行好好的,现在你非要加入个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性会降低。

  2. 系统复杂度提高
    加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。

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

  4. 消息顺序问题
    如果有 A、B 两个消息,B 消息被消费者消费的前提是 A 消息已被执行,这时候就不能先执行 B,得先执行 A 才行。

那怎么解决以上缺点呢?

  1. 系统可用性降低:集群模式保证高可用。
    镜像集群模式:这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。

  2. 系统复杂度提高
    加入 Rabbit 确实会增加系统复杂度,但 MQ 的解耦、提速、削峰这些方面的收益超过管理 MQ 的成本,所以该用还得用。

  3. 一致性问题
    RabbitMQ 分布式消息是最终一致性的,即使可能因为消息失败而导致前后消息不一致,但分布式系统是在不同服务器上的,不能像简单的本地回滚一样,所以它通过发送延迟消息和定时消息来进行消息补偿,保证最终消息是一致性的,即是一个完整的事务。
    RabbitMQ 本身是有事务的功能的,但是分布式的事务处理效率太低,且发生问题的可能性不高,所以多是选择放弃强一致性,而采用最终一致性。

  4. 消息顺序问题
    在 MQ 中将有顺序要求的 AB 两个消息分别用两个队列与两个消费端手工 ack 接收,并且在消息上需要有对应的同组编号信息,以及发送次数,如果 B 执行的前提是已经消费了 A,那需要在消费端判断 A 消息是否已经正确接收(也就是查询成功的消息库)。如果 A 已经消费成功,则消费 B,如果 A 消费失败,或者 A 还没有消费,则 B 消息也直接返回为消息失败,并且不重回队列。并且让消息提供方重新发送 AB 消息,如果连续三次发送消息仍然消费失败,则 AB 两个消息第四次处理时就扔入死信队列中,等待人工处理。

RabbitMQ 事务是怎么实现的?

事务的实现主要是对信道(Channel)的设置,主要的方法有三个:

  1. channel.txSelect() 声明启动事务模式;

  2. channel.txComment() 提交事务;

  3. channel.txRollback() 回滚事务;

RabbitMQ 的实现原理是怎么样子的?

首先,消息提供方会和 RabbitMQ 之间建立起 TCP 连接(Connection),每个连接中会有多个通信的信道(channel)来提连通信效率,不同的信道之间通过信道 id 来实现信息隔离。

而在 RabbitMQ 中,消息提供者发出的消息会先到 RabbitMQ 中的交换机中,由交换机根据分发规则,通过队列的形式分发消息。

消息接收方与 RabbitMQ 之间也一样是通过 TCP 连接和信道建立连接和通信。

此外,在 RabbitMQ 中,交换机与队列之间不同的边接方式,也产生了不同的工作模式。

RabbitMQ 有哪些工作模式?

  1. 简单模式:就是不通过交换机,消息直接通过队列,一对一收发。

  2. 工作队列模式:也是不通过交换机,消息直接通过队列,只是一个发送方可以有多个接收端。

  3. 发布订阅模式:由交换机分发消息到不同队列,每个消费者只监听自己的队列。

  4. 路由模式:由交换机分发消息,但是发送方需要指定路由 key,交换机会根据不同的 routing key 分发给不同的队列,消费方对应自己需要的队列。

  5. 通配符模式:和路由模式有些相近,只是通配符模式可以在绑定 routing key 时使用通配符。

  6. RPC 模式:RPC 远程调用模式,严格来说不太算是 MQ。

为什么 RPC 严格来说不能算是 MQ?

RPC,远程过程调用,实际上是一种技术思想,而一种规范或协议,一般来说 RPC 远程调用是同步通信的,且 RPC 模式是没有队列的,多用在立即等待返回处理结果的场景,比如使用基于 RPC 思想的 Dubbo。而 MQ 是用来异步提速的,所以严格来说,RPC 模式不能算是 MQ。

消息怎么路由?

  • 消息提供方 ->路由 ->一至多个队列消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定。通过队列路由键,可以把队列绑定到交换器上。消息到达交换器后,RabbitMQ 会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则);

  • 常用的交换器主要分为一下三种:

    • fanout:如果交换器收到消息,将会广播到所有绑定的队列上。
    • direct:如果路由键完全匹配,消息就被投递到相应的队列。
    • topic:可以使来自不同源头的消息能够到达同一个队列。 使用 topic 交换器时,可以使用通配符。

消息基于什么传输?

由于 TCP 连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ 使用信道的方式来传输数据。信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制。

RabbitMQ 中的交换机可以储存消息吗?

不可以。交换机只负责转发消息,不具备存储消息的能力。

如何防止消息丢失呢?

  1. 在生产者丢失——confirm 确认模式

    1. 使用 RabbitMQ 事务机制,但它是同步的,且很耗性能。
    2. 开启 confirm 确认模式,确认消息是否从“生产者”发送到“交换机”,成功回传 ack 消息,失败可以重试或抛异常。且 confirm 模式是异步回调接口通知 MQ 是否接收到消息。一般都采用这种方式。
  2. 在 MQ 中丢失——持久化

    1. 开启 RabbitMQ 持久化,防止 RabbitMQ 自己弄丢数据。除非极小概率还没来得及持久化,MQ 就先挂了,即使这样,也只会丢失极少的数据量。
    2. 所以,持久化可以跟生产者那边的 confirm 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 ack 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 ack,你也是可以自己重发的。
    3. 但持久化的过程也是很耗性能的。
  3. 在消费者丢失——ack 机制

    1. 用 RabbitMQ 提供的 ack 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 ack,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 ack 一把。这样的话,如果你还没处理完,不就没有 ack 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。以上三种方式,只是防止丢失,但具体补消息,还需要靠消息的补偿机制(也就是消息的可靠性保证)。

❗RabbitMQ 如何保证消息的可靠性呢?

消息补偿。(延迟消息 + 定时扫描)

  • 由生产者发送消息给 MQ,MQ 会将消息给到消费者,消息者收到消息后,会返回一个消息确认,这个“消息确认”会被 MQ 放到“回调检查服务”中,“回调检查服务”会将收到的消息给到定时检查的 MDB。

  • 但在这个过程中,如果出现异常或者网络波动,就会导致消息到不了回调检查服务,所以为了保证能够消息可靠性,会由生产者延迟一段时间后,再发送一个相同的消息给 MQ,这个消息会直接被 MQ 发送到回调检查服务。

  • 回调检查服务会将延迟发送的消息和 MDB 中的消息对比,如果 MDB 中没有该消息,就会调用生产者,让生产者重新发送消息。

  • 但在以上过程,还可能出现“延迟发送消息”也出问题,为了更深层保证消息的可靠性,还需要一个定时检查服务,每隔一段固定时间,定时检查服务会将 MDB 里的消息和 DB 中的消息进行匹配(检查某个时间段的表,而不是全表扫描),如果有 MDB 缺失的消息,就会调用生产者重新发送消息。

什么是 TTL?什么是死信队列,消息成为死信有哪几种情况?什么是延迟队列?

  • TTL:全称 Time To Live(存活时间/过期时间)

    1. 如果给消息设置过期时间,即使到了过期时间,消息也不会立马被清除,只有等消息到了队列的头上,才会被判断是否过期清除。
    2. 如果给整个队列设置过期时间,即每一个进入队列的消息,都会各自被设置为了相同的过期时间。而非整个队列定时隔一段时间清除。
    3. 如果单独消息和整个队列两则都设置了过期时间,以时间短的为准。
  • 死信队列:英文缩写:DLX  。Dead Letter Exchange(死信交换机)

    • 消息成为死信的三种情况:
      1. 队列消息长度到达限制;
      2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
      3. 原队列存在消息过期设置,消息到达超时时间未被消费;
  • 延迟队列:即消息进入队列后不会立即被消费,只有到到达指定时间后,才会被消费。

    • RabbitMQ 中并未提供延迟队列功能,采用【TTL】+【死信队列】 组合实现延迟队列的效果。

有几百万消息持续积压几小时,说说怎么解决?

消息积压处理办法:临时紧急扩容:

先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。

临时建立好原先 10 倍的 queue 数量。

然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。

接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。

等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。

消息队列满了以后该怎么处理

没办法了,说明紧急扩容也来不及了,只能“丢弃 + 批量重导”了,写程序快速消费,然后重导。

RabbitMQ 如何保证消息的不重复消费呢?【重要】

一条数据重复出现两次,数据库里就只有一条数据,这就是保证了系统的幂等性。

保障了消息的幂等性,同一条消息被重复消费也就不影响了,因为不影响最终执行结果。

  1. 方法一:采用乐观锁机制保证消息幂等性。在数据库中会增加一个版本字段,执行时也会匹配版本,如果版本不一致,SQL 语句的匹配就不成立,就不会执行。

  2. 方法二:你拿到这个消息做数据库的 insert 操作,那就容易了,给这个消息做一个唯一的主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。

  3. 方法三:你拿到这个消息做 redis 的 set 的操作,那就容易了,不用解决,因为你无论 set 几次结果都是一样的,set 操作本来就算幂等操作。

  4. 方法四:如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以 redis 为例,给消息分配一个全局 id,只要消费过该消息,将 <id,message> 以 K-V 形式写入 redis。那消费者开始消费前,先去 redis 中查询有没有消费记录即可,先根据这个 id 去比如 redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。

如何解决消息队列的延时以及过期失效问题?

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

我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上 12 点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据(发送的数据库和确认接收的数据库匹配),写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。

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

如果要你自己设计一个 MQ,你会怎么设计?

  1. 重启起一个服务器来(使用 Redis)充当 RabbitMQ,实现 MQ 的解耦功能。

  2. 这个服务器需要可以存储和转发消息,存储是为了实现永久化,防止服务器出问题,消息丢失;转发消息则是可以通过不同队列来实现指定消费端,实现异步提速功能和削峰填谷的功能。

  3. 建立消息补尝机制,防止消息丢失。

怎么使用 Redis 实现 MQ 功能呢?

  1. 使用 list 类型保存数据信息,rpush 生产消息,lpop 消费消息,当 lpop 没有消息时,可以 sleep 一段时间,然后再检查有没有信息,如果不想 sleep 的话,可以使用 blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。redis 可以通过 pub/sub 主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。

  2. 使用 sortedset,使用时间戳做 score, 消息内容作为 key,调用 zadd 来生产消息,消费者使用 zrangbyscore 获取 n 秒之前的数据做轮询处理。

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
访客数 访问量