MQ专题
# MQ专题
# 为什么要用MQ
面试官:为什么要使用MQ呢,他的作用是什么
我:MQ的作用主要有三个解耦,异步,削峰
# 1.1 MQ的作用
# 1.1.1 解耦
如果多个模块或者系统中,互相调用很复杂,维护起来比较麻烦,但是这个调用又不是同步调用,就可 以运用MQ到这个业务中。
# 1.1.2 异步
这个很好理解,比如用户的操作日志的维护,可以不用同步处理,节约响应时间。
# 1.1.3 削峰
在高峰期的时候,系统每秒的请求量达到 5000,那么调用MySQL的请求也是5000,一般情况下 MySQL 的请求大概在2000左右,那么在高峰期的时候,数据库就被打垮了,那系统就不可用了。此时引入MQ,在系统A前面加个MQ,用户请求先到MQ,系统A从MQ中每秒消费2000条数据,这样就把本来5000的请求变为 MySQL 可以接受的请求数量了,可以保证系统不挂掉,可以继续提供服务。MQ 里的数据可以慢慢的把它消费掉。
# 1.2 使用了MQ会有什么问题
面试官:MQ使用后可能会出现什么问题呢?
我:
- 增加了系统的复杂度:因为一个系统引入了MQ之后会造成系统的复杂性的提升,复杂性提升后, 增加MQ的维护成本
- 降低的系统的可用性:复杂性的提升意味这系统可用性的降低,因为MQ一旦出现问题就会造成系统出现问题。
- 一致性问题:因为MQ是异步处理消息,需要处理类似于消息丢失以及重复消费的问题,一旦处理不好就会造成重复消费问题。
# 1.3 如何避免MQ消息堆积
面试官:如何避免MQ消息堆积
# 1.3.1 产生MQ消息堆积的原因
- 生产者投递消息的速率与我们消费者消费的速率完全不匹配。
- 生产者投递消息的速率>消费者消费的速率 导致我们消息会堆积在我们 mq 服务器端中,没有及时的被消费者消费,所以就会产生消息堆积的问题
- 注意的是:rabbitmq消费者我们的消息消费如果成功的话消息会被立即删除。 kafka或者rocketmq消息消费如果成功的话,消息是不会立即被删除。
# 1.3.2 解决办法
提高消费者消费的速率;(对我们的消费者实现集群)
消费者应该批量形式获取消息 减少网络传输的次数
# 1.4 为什么会出现重复消费?
面试官:为什么MQ会出现重复消费,以及如何来解决重复消费问题
我:MQ的消息流程主要有两个阶段来完成,发送消息到消息队列以及消息队列将消息投递到消费者,因为各种网络原因会造成发送方与以及接收方消息重试,就会造成重复消费
# 1.4.1 发送方消息重试
因为网络原因以及MQ自身原因会导致发送的消息未成功投递到MQ,如果出现未成功投递就会重复投 递消息,这个时候是正常的,但是可能因为网络原因导致消息确认消息延时,实际上已经投递到了MQ 但是确认消息没有己实被发送方接收到,就会重新发起消息重试,这样就会造成消息重复,这种是发送 方消息重试,但是发送方重试次数是有限制的,如果达到一定次数后还是失败如果没有做其他处理就会 造成消息丢失。
# 1.4.2 消费方消息重试
当消息正常投递到MQ后,就需要消费消息了,正常情况下消息会被发送到消费方进行消费,消费方一 般需要开启手动确认来确定消息一定会被消费掉,但是可能网络原因导致消息未被消费,这个时候消费 方就会重试消费消息,如果是出现消费超时或者异常就会进行消息重复消费,如果异常没有处理好导致 消息重复消费可能造成业务出现幂等性问题,但是如果做了幂等性问题,多次消费还是失败就会造成消 息丢失。
# 1.5 如果出现重复消费如何解决
面试官:如何解决重复消息问题
# 唯一主键
手动给每一条消息增加一个唯一的消息ID,使用消息ID作为唯一主键,如果消息重复,消息插入不进 去,用这种方式来解决消息重复
# 使用幂等
使用幂等方式来解决重复消费问题,手动给每一条消息增加一个唯一的消息ID,不要使用系统生成的消息ID,如果生成消息后就将消息保存到Redis中,如果消息消费成功后删除Redis中的消息,如果失败了不需要管Redis中的消息,这样就完来实现消息的幂等。

# 1.6 如何保证消息顺序性
面试官:MQ如何保证消息的有序消费
# 1.6.1 问题解析
一个queue,有多个consumer去消费,这样就会造成顺序的错误,consumer从MQ里面读取数据是有 序的,但是每个consumer的执行时间是不固定的,无法保证先读到消息的consumer一定先完成操 作,这样就会出现消息并没有按照顺序执行,造成数据顺序错误,并且执行的时候是多线程执行的,并 不能保证执行的顺序性。

# 1.6.2 解决办法

# 1.7 如何实现延时消息
面试官:说下你所知道的实现消息延时发送的方法
我:延时消息就是一个消息需要延时多长时间才能够发送,比如用户支付订单,如果多长时间未支付就会取消订单,这个就属于延时消息
# 延时消息实现方式
# 数据库轮询
通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行update或delete等操作
# JDK的延迟队列

利用JDK自带的DelayQueue来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中 获取元素,放入DelayQueue中的对象,是必须实现Delayed接口的。
# netty时间轮算法

# RabbitMQ
可以采用RabbitMQ的死信队列来实现延时队列,RabbitMQ可以针对Queue和Message设置x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter,dead letter会被投递到死信交换器,然后通过死信队列将消息发送出去

# RocketMQ
RocketMQ可以实现18个等级的消息延时,但是不可以实现任意时间的消息延时,使用RocketMQ的延 时消息只需要按照正常消息发送,并指定延时等级即可,简单高效,并且这个延时时间可以在 RocketMQ的配置参数中进行配置。
# 怎样选型MQ
面试官:你是如何选型MQ的能说下RocketMQ,RabbitMQ,Kafka三个MQ的特点吗
# 2.1 需求分析
# 2.1.1 功能需求
除了最基本生产消费模型,还需要MQ能支持REQUEST-REPLY模型,以提供对同步调用的支持。 此 外,如果MQ能提供PUBLISH-SUBSCRIBE模型,则事件代理的实现可以更加简单。
# 2.1.2 性能需求
考虑未来一到两年内产品的发展,消息队列的呑吐量预计不会超过 1W qps,但由单条消息延迟要求较高,希望尽量的短。
# 2.1.3 可用性需求
因为是在线服务,因此需要较高的可用性,但充许有少量消息丢失。
# 2.1.4 易用性需求
包括学习成本、初期的开发部署成本、日常的运维成本等。
# 2.2 横向对比
我:几个MQ的主要对比如下如
| 特性 | ActiveMQ | RabbitMQ | Kafka | RocketMQ |
|---|---|---|---|---|
| PRODUCER- COMSUMER | 支持 | 支持 | 支持 | 支持 |
| PUBLISH-SUBSCRIBE | 支持 | 支持 | 支持 | 支持 |
| REQUEST-REPLY | 支持 | 支持 | - | 支持 |
| API完备性 | 高 | 高 | 高 | 低(静态配置) |
| 多语言支持 | 支持,Java优先 | 语言无关 | 支持,Java优先 | 支持 |
| 单机呑吐量 | 万级 | 万级 | 十万级 | 单机万级 |
| 消息延迟 | 毫秒级 | 微秒级 | 毫秒级 | 毫秒级 |
| 可用性 | 高(主从) | 高(主从) | 很高(分布式) | 非常高(分布式) |
| 消息丢失 | 低 | 理论上不会丢失 | 理论上不会丢失 | |
| 消息重复 | - | 可控制 | 理论上会有重复 | 允许重复 |
| 文档的完备性 | 高 | 高 | 高 | 中 |
| 提供快速入门 | 有 | 有 | 有 | 无 |
| 首次部署难度 | - | 低 | 中 | 高 |
# 2.3 选型参考
| MQ | 描述 |
|---|---|
| RabbitMQ | erlang开发,对消息堆积的支持并不好,当大量消息积压的时候,会导致 RabbitMQ 的性能急剧下降。每秒钟可以处理几万到十几万条消息 |
| RocketMQ | java开发,面向互联网集群化功能丰富,对在线业务的响应时延做了很多的优化, 大多数情况下可以做到毫秒级的响应,每秒钟大概能处理几十万条消息 |
| Kafka | Scala开发,面向日志功能丰富,性能最高。当你的业务场景中,每秒钟消息数量 没有那么多的时候,Kafka 的时延反而会比较高。所以,Kafka 不太适合在线业务场景。 |
| ActiveMQ | java开发,简单,稳定,性能不如前面三个。小型系统用也ok,但是不推荐。推荐 用互联网主流的 |
# 2.4 业务为什么使用rocketmq 不用kafka
面试官:为什么公司一般使用RocketMQ而使用kafka的不是很多呢
我:因为kafka的诞生是作为大数据的一个中间件来使用的,MQ只是kafka的一个功能,相对于 RocketMQ,Kafka的功能相对于简单,RocketMQ设计的时候借鉴了很多kafka的设计,有着后发优 势,支持分布式、集群、副本等机制,消息延时也比较少,但是RocketMQ也有很多kafka不具备的功 能,比如:严格顺序消息,延时消息,服务端tag过滤,能够至少保证消费一个的可靠性策略,消息失 败后重试时间随着次数递增等都是很多业务所需要的,并且RocketMQ是经过了双十一的验证,所以很 多人说RocketMQ是专门为业务而生的。
# 2.5 为什么kafka不能支持大量的topic
面试官:为什么kafka对于大量的topic支持不好呢,能详细的说下吗
我:这句话如果严格意义上说的话应该是kafka在topic-partition过多而不是单纯的topic过多
这个要从kafka的存储结构来说起,kafka的最小存储单元是一个partition,一个partition由一个数据文 件以及一个索引文件组成,partition的存储文件是通过顺序写的方式来保证写入磁盘的效率的,但是如 果非常多的partition需要磁盘写入,那么就可能造成顺序写变成随机写,因为磁盘的IO流量就那麽大, 同时由很多个partition需要写入,就会变成随机写,性能反而会急剧下降,并且partition过多还是导致 元数据管理,副本同步的困难,这些都是导致kafka不能支持大量topic的原因。
# 2.6 为什么RcoketMQ可以支持大量的Topic
面试官:为什么RocketMQ可以支持大量的Topic呢
我:这个需要从RocketMQ的存储结构来说,RocketMQ的所有的数据存储在一个commitlog,然后则会 通过异步线程将commitlog的数据转移到其他对应的消费者的队列中,因为不管多少个队列也只有一个 commitlog文件,所有写入以及读取性能不会随着队列的增加而有显著的变化

2.6.1 下面是RocketMQ和kafka的topic的测试报告

# RabbitMQ交换器有哪些特点是什么
主要有以下4种
1.fanout
把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。
2.direct
把消息路由到BindingKey和RoutingKey完全匹配的队列中。
3.topic
Topic交换器能够根据路由键匹配根据规则匹配相关的交换器,主要的表达式有#和*
4.headers
不依赖路由键匹配规则路由消息。是根据发送消息内容中的 headers 属性进行匹配。性能差,基本用不到。
# 3.1 Virtual Host是什么
每一个RabbitMQ服务器都能创建虚拟的消息服务器,也叫虚拟主机(virtual host),简称vhost。
# 3.2 RabbitMQ集群中的节点类型有哪些
面试官:RabbitMQ的集群模式有哪些方案,以及节点类型有哪些
我:集群模式有以下几种
单机版RabbitMQ
集群模式,普通集群、高可用集群
镜像队列
节点类型有以下几种
内存节点ram,将变更写入内存。
磁盘节点disc,磁盘写入操作。
RabbitMQ要求最少有一个磁盘节点。
# 3.3 导致的死信的几种原因
消息被拒( Basic.Reject /Basic.Nack ) 且 requeue = false 。
消息TTL过期。
队列满了,无法再添加。
# 3.4 RabbitMQ如何保证消息的可靠性
面试官:如何保证RabbitMQ的消息可靠性。
1.生产者到RabbitMQ
失败通知,发布者确认,备用交换器。
2.RabbitMQ自身
持久化、高可用集群模式,镜像模式。
3.RabbitMQ到消费者
basicAck机制、死信队列、消息补偿机制。
# 什么是RocketMQ的事务消息
面试官:什么是RocketMQ的事务消息
事务消息就是MQ提供的类似XA的分布式事务能力,通过事务消息可以达到分布式事务的最终一致性。
半事务消息就是MQ收到了生产者的消息,但是没有收到二次确认,不能投递的消息。
# 4.1 实现原理
生产者先发送一条半事务消息到MQ
MQ收到消息后返回ack确认
生产者开始执行本地事务
如果事务执行成功发送commit到MQ,失败发送rollback
如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查
生产者查询事务执行最终状态
根据查询事务状态再次提交二次确认
最终,如果MQ收到二次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存下来并且在3天后被删除。

# 4.2 Broker是怎么保存数据的呢
面试官:RocketMQ的存储结构是什么样的。

RocketMQ主要的存储文件包括commitlog文件、consumequeue文件、indexfile文件。
Broker在收到消息之后,会把消息保存到commitlog的文件当中,而同时在分布式的存储当中,每个 broker都会保存一部分topic的数据,同时,每个topic对应的messagequeue下都会生成 consumequeue文件用于保存commitlog的物理位置偏移量offset,indexfile中会保存key和offset的对 应关系。
# 4.3 RocketMQ如何保证消息的可靠性
# 4.3.1 生产者丢失
生产者可能因为网络原因,broker故障等原因丢失消息,使用同步发送不会出现这种问题,因为只要开 启master的同步刷盘,异步复制,基本上能够保证消息不会丢失,如果消息不能落盘到master中,这 消息就不会发送成功,并且发送端有发送方重试,如果消息发送失败会给出消息发送失败,我们在做处 理就可以的。
# 4.3.2 MQ丢失
如果生产者保证消息发送到MQ,而MQ收到消息后还在内存中,这时候宕机了又没来得及同步给从节点,就有可能导致消息丢失。
RocketMQ分为同步刷盘和异步刷盘两种方式,默认的是异步刷盘,就有可能导致消息还未刷到硬盘上 就丢失了,可以通过设置为同步刷盘的方式来保证消息可靠性,这样即使MQ挂了,恢复的时候也可以从磁盘中去恢复消息。
# 4.3.3 消费者丢失
==消费者丢失消息的场景==
消费者刚收到消息,此时服务器宕机,MQ认为消费者已经消费,不会重复发送消息,消息丢失。
RocketMQ默认是需要消费者回复ack确认,消费方不返回ack确认,重发的机制根据MQ类型的不同发 送时间间隔、次数都不尽相同,如果重试超过次数之后会进入死信队列,需要手工来处理了,消息在死 信队列的时间一般在72小时,如果不及时处理可能造成消息丢失。
# 4.4 RocketMQ如何保证的可靠性的
面试官:RocketMQ是如何保证集群的可靠性
我:RocketMQ在设计之出就保证了消息至少被消费一次,我们只需要保证消息能够正常投递到MQ中就可以的
# 4.4.1 发送方保证消息的可靠性
发送的的时候由三种方式,同步,异步,单向,其中同步异步都有发送发确认机制,如果发送失败会进行重试,而单向发送则只管发送不管结果
# 4.4.2 RocketMQ保证消息可靠性

RocketMQ由nameserver和broker组成,nameserver之间是不互相通讯的,这样当一个 nameserver挂掉后会找到另一个nameserver重试消息
RocketMQ的broker是一个主从架构,可以配置一主一从或者多主多从,MQ的主节点收到消息后 通过同步策略会将消息复制到从节点中。
RocketMQ的消息同步策略有同步复制和异步复制,而如果要保证消息不丢失可以选择同步复制, 只有消息同步复制到了从节点后才会确认消息发送到MQ成功
Broker 故障规避,默认情况下延迟规避策略只在重试时生效,例如在一次消息发送过程中如果遇 到消息发送失败,规避 broekr-a,但是在下一次消息发送时,即再次调用 DefaultMQProducer 的 send 方法发送消息时,还是会选择 broker-a 的消息进行发送,只要继续发送失败后,重试时再次规避 broker-a。
RocketMQ的存储方式有两种同步刷盘以及异步刷盘,同步刷盘是指消息必须写入磁盘后才会成 功,而异步刷盘是指消息必须只要写入内存就是成功,但是在还没有写入磁盘的过程中如果断电可 能造成部分消息丢失
# 4.4.3 消费端保证消息不丢失

RocketMQ为了保证消费进度不丢失使用了只记录最小的消费进度,比如消费端消费了100-200之 间的消息,如果第101条消息没有消费成功101后面的所以消息消费成功,如果这个时候消费者重 启也会导致消息从101-200之间的所有消息进行重试
为了防止消息一直重试的问题,引入了死信队列,当消息一直消费不了的时候就会进入死信队列交 给死信队列处理
如果消费成功后的消息因为删库跑步导致数据丢失,还可以通过RocketMQ的消息回溯功能将以前 消费过的消息在消费一遍
# 4.5 为什么RocketMQ不使用Zookeeper作为注册中心呢
根据CAP理论,同时最多只能满足两个点,而zookeeper满足的是CP,也就是说zookeeper并不能 保证服务的可用性,zookeeper在进行选举的时候,整个选举的时间太长,期间整个集群都处于不可用的状态,而这对于一个注册中心来说肯定是不能接受的,作为服务发现来说就应该是为可用性而设计
基于性能的考虑,NameServer本身的实现非常轻量,而且可以通过增加机器的方式水平扩展,增 加集群的抗压能力,而zookeeper的写是不可扩展的,而zookeeper要解决这个问题只能通过划分 领域,划分多个zookeeper集群来解决,首先操作起来太复杂,其次这样还是又违反了CAP中的A 的设计,导致服务之间是不连通的。
持久化的机制来带的问题,ZooKeeper 的 ZAB 协议对每一个写请求,会在每个 ZooKeeper 节点 上保持写一个事务日志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的一 致性和持久性,而对于一个简单的服务发现的场景来说,这其实没有太大的必要,这个实现方案太重了。而且本身存储的数据应该是高度定制化的。
消息发送应该弱依赖注册中心,而RocketMQ的设计理念也正是基于此,生产者在第一次发送消息的时候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可用,短时间内对于生产者和消费者并不会产生太大影响。