梁越

消息队列面试常问

0 人看过

面试喜欢问消息队列,特别是大厂

什么是消息队列

我们都知道队列,一端入队,一端出队。消息队列也是类似的结构,一端生产者只负责往队列里发送消息数据,另一端消费者只负责从队列里获取数据,获取方式可能是队列推送或者消费者拉取

消息队列相关概念

  1. 生产者(Producer):负责产生消息
  2. 消费者(Consumer):负责消费消息
  3. 消息(Message):在应用间传送数据,消息可以很简单,字符串或者整型,也可以很复杂,自定义对象和函数
  4. 消息队列(Message Queue):一种应用间的通信方式,消息发送后可以立即返回,由消息系统来确保消息的可靠传递
  5. 消息代理(Message Broker):负责存储/转发消息,转发分为推和拉两种
  • 拉是指Consumer主动从Message Broker获取消息;
  • 推是指Message Broker主动将Consumer感兴趣的消息推送给Consumer。

消息队列的作用是什么

  1. 异步

对于以下同步的操作,时间t=t(用户注册)+t(注册写入)+t(发送注册邮件)+t(发送注册短信)

如果使用消息队列来异步发送,时间t=t(用户注册)+t(注册写入)+max(t(发送注册邮件), t(发送注册短信)),之前是同步发送,现在发送的事情交给消息队列来处理

  1. 削峰

削峰其实就是请求太多了,一下子处理不完,甚至可能太多请求压垮服务器或者数据库,例如双十一,亿级的请求,50w的qps如果直接打在数据库,基本宕机了,但是如果使用消息队列存放,消费者用可以接受的最快速度进行消费就行了,然后过了峰值时间,消费者最后会消费完所有数据。

再比如使用RocketMQ,有一个点赞业务,不限制用户的点赞数只需进行记录(产品需求,开发提议无效),当每个用户都进行x连击享受数量猛增的快感时如果数据库都需要进行x个点赞数据的插入,数据库毫无疑问会塞死导致崩溃。

于是想到可以尝试下MQ削峰,比如每秒来了5000消息但数据库只能承受2000,那我消费时每次只拉取消费1600就好了,剩下的放在Broker堆积慢慢消费就好。由于之前的消息中心也在用RocketMQ,于是确认使用RocketMQ来进行削峰。

  1. 解耦

举个例子,A公司做了某个系统,B公司觉得A公司的某个功能很好,于是B公司和A公司的系统进行了集成。这时C公司也觉得A公司的这个功能很好,于是,C公司也和A公司的系统进行了集成。以后还有D公司…。

介于这种情况,A公司的系统和其他公司的耦合度都很高,每集成一个公司的系统,A公司都需要修改自己的系统。如果采用消息队列,则变成了如下:

不管以后还有多少公司的应用程序想要用A公司的程序,都不需要和A公司进行集成,谁需要这个功能,谁就去消息队列里面获取

消息队列的消费场景

对于消息队列里的消息,可能有重复的,可能消息会在入队时丢失,也可能就是刚好消费一次

无限制

该场景是最容易满足的,特点是整个消息队列吞吐量大,实现简单。适合能容忍丢消息,消息重复消费的任务。

  1. Producer发送消息到Message Broker阶段:

    Producer发消息给Message Broker,不要求Message Broker对接收到的消息响应确认,Producer也不用关心Message Broker是否收到消息了。

  2. Message Broker存储/转发阶段:

    对Message Broker的存储不要求持久性,转发消息时也不用关心Consumer是否真的收到了。

  3. Consumer消费阶段:

    Consumer从Message Broker中获取到消息后,可以从Message Broker删除消息,或Message Broker在消息被Consumer拿去消费时删除消息,不用关心Consumer最后对消息的处理结果。

消息至少被消费一次

适合不能容忍丢消息,但允许重复消费的任务。

  1. Producer发送消息到Message Broker阶段:

    Producer发消息给Message Broker,Message Broker必须响应对消息的确认。

  2. Message Broker存储/转发阶段:

    Message Broker必须提供持久性保障,转发消息时,Message Broker需要Consumer通知删除消息,才能将消息删除。

  3. Consumer消费阶段:

    Consumer从Message Broker中获取到消息,必须在消费完成后,Message Broker上的消息才能被删除。

消息仅被消费一次

  1. Producer上产生的消息被Consumer仅消费一次,场景要求:
  • Producer发送消息到Message Broker阶段:

    Producer发消息给Message Broker,Message Broker必须响应对消息的确认,并且Producer负责为该消息产生唯一标识,以防止Consumer重复消费(因为Producer发消息给Message Broker后,由于网络问题没收到Message Broker的响应,可能会重发消息给到Message Broker)。

  • Message Broker存储/转发阶段:

    Message Broker必须提供持久性保障,并且每条消息在其消费队列里有唯一标识(这个唯一标识需要由Producer产生)。

  • Consumer消费阶段:

    Consumer从Message Broker中获取到消息后,需要记录下消费的消息标识,以便在后续消费中防止对某个消息重复消费。比如Consumer获取到消息,消费完后,还没来得及从Message Broker删除消息,就挂了,这样Message Broker如果把消息重新加入待消费队列的话,那么这条消息就会被重复消费了。

  1. Producer上产生的消息被Consumer仅消费一次,场景要求:
  • Producer发送消息到Message Broker阶段:

    Producer发消息给Message Broker,Message Broker必须响应对消息的确认,并且Producer负责为该消息产生唯一标识,以防止Consumer重复消费(因为Producer发消息给Message Broker后,由于网络问题没收到Message Broker的响应,可能会重发消息给到Message Broker)。

  • Message Broker存储/转发阶段:

    Message Broker必须提供持久性保障,并且每条消息在其消费队列里有唯一标识(这个唯一标识需要由Producer产生)。

  • Consumer消费阶段:

    Consumer从Message Broker中获取到消息后,需要记录下消费的消息标识,以便在后续消费中防止对某个消息重复消费。比如Consumer获取到消息,消费完后,还没来得及从Message Broker删除消息,就挂了,这样Message Broker如果把消息重新加入待消费队列的话,那么这条消息就会被重复消费了。

消息队列的实际使用场景

秒杀活动,双十一削峰,点赞数更新等

消息队列都有哪些组件

目前在市面上比较主流的消息队列中间件主要有,Kafka、ActiveMQ、RabbitMQ、RocketMQ 等这几种。

ActiveMQ和RabbitMQ这两着因为吞吐量还有GitHub的社区活跃度的原因,在各大互联网公司都已经基本上绝迹了,业务体量一般的公司会是有在用的,但是越来越多的公司更青睐RocketMQ这样的消息中间件了。

Kafka和RocketMQ一直在各自擅长的领域发光发亮,目前用的比较多

消息队列组件之间的区别和使用场景

至于各个组件之间的区别,如下图:

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

RabbitMQ的高可用

RabbitMQ的高可用是基于主从(非分布式)做高可用性。RabbitMQ 有三种模式:单机模式(Demo级别)、普通集群模式(无高可用性)、镜像集群模式(高可用性)。

RabbitMQ的普通集群模式

就是多机部署多个RabbitMQ实例,每个机器上运行一个实例,但是真正保存消息队列的只有一台机器上的一个实例,其他实例保存的是消息队列的元数据,其他机器可以从主实例拉取消息然后返回。所以整个集群只有一个消息队列,但是可以多台机器访问,看起来只是提高了吞吐量。

RabbitMQ的镜像集群模式

这个模式才是真正的高可用,每个机器也是一个实例,但是实例都保存同一个消息队列,相当于一个消息队列有多个备份,所以主要压力时生产者需要同步所有实例,所以网络带宽压力更大了。

Kafka的高可用

Kafka的天然架构就是适合分布式的,Kafka 一个最基本的架构认识:由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。

Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。

如果某个 broker 宕机了,那个 broker上面的 partition 在其他机器上都有副本的。如果这个宕机的 broker 上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。

写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)

消费的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。

使用redis实现消息队列

这里转发一下知乎的高赞回答,由于篇幅问题,我将搬运在另一篇文章

参考链接

https://mp.weixin.qq.com/s/8prxhEfcc4b3RFBjFLhk9g
https://cloud.tencent.com/developer/article/1629610
https://www.zhihu.com/question/34243607
https://juejin.cn/post/6844903993081085965