面试技巧:你需要去考虑一下你负责的系统中是否有本篇文章类似的场景,尽量去贴近实战去讲解
为什么要用MQ(MQ的优点)
首先要明白面试官问你这个问题主要是考察的点有哪些:
- 你们公司有个什么场景(需要用到MQ)
- 这个场景有个什么样的技术挑战(如果不用MQ可能会很麻烦),然后开始将MQ带来的好处
解耦
image.png
- 解耦:一个系统A产生的数据,其他系统如果需要该数据,自己消费即可,不需要A系统去各个系统去做交互,也不用考虑其他系统服务出现问题做一些重试的处理、失败超时等问题。如果某个系统不需要该数据,则取消订阅即可,这样A系统就可以不需要过度依赖其他系统。其他系统也不会影响A系统。代码维护成本也会降低。
总结:通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。
异步
image.png
- 异步:A系统/模块同步调用各个相关系统/模块,相对来说同步调用花费时间较长,因为同步调用需要各个依赖系统/模块逐一完成接口调用才能结束。这样耗时较长,用户无感知的时间大概是200ms。如果利用MQ将A系统/模块调用各个系统的API通过MQ队列进行异步化,这样可以大幅缩减调用时间,提高高延时接口的速度。
削峰
image.png
- 削峰:在没有MQ的情况下系统A,在高峰期大量请求会涌入系统中,如果系统直接接入的MySQL,这样MySQL会直接崩掉。MySQL如果崩掉整个系统就会崩掉。这是在高峰期的情况也就是峰值,若是平常低谷时期整个系统是没有问题的。如果使用了MQ可以在峰值的时候,将所有请求都写入到MQ,假设系统A每秒钟最多只能处理两千请求,我们就可以在高峰期的时候每秒每次从MQ中最多拉取两千个请求,这样就能避免系统崩掉。但是这样会导致大量请求积压在MQ中,但是其实是可以接受。只要高峰期一过,在低谷时完全可以将积压的数据消费掉。
消息队列的缺点
系统引入MQ会引发什么样的问题?
- 可用性降低:MQ一旦发生问题,就会导致生产者无法发送消息,其它消费者系统就无法消费消息 。导致整个系统就挂了
- 复杂性变高:生产者系统/模块同样的数据生成两条,发送了两次(消息幂等性);生产者系统/模块给MQ发送的消息还没有被消费者系统/模块消费,数据就丢了(可靠性);生产者系统/模块发送给MQ的消息是顺序发送的,结果MQ导致顺序错乱,从而无法保证消费者系统/模块消费数据的顺序性(顺序性);消费者挂了导致MQ积压大量的数据。
- 一致性的问题:各个系统/模块执行的结果不一致,有的成功,有的失败,但是用户得到的是成功。其实最终的正确的结果应该是失败的。
各个MQ的优缺点
特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 |
topic 数量对吞吐量的影响 | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | ||
时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 |
可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ |
功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 |
综上,各种对比之后,有如下建议:
一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了;
后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高;
不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。
所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。
如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。
引入消息队列之后应该如何保证其高可用性
Rabbit MQ的高可用性
Rabbit MQ有三种模式:
- 单机模式-(demo级别的,生产很少使用)
- 普通集群模式
- 镜像集群模式
普通集群模式(非分布式非高可用):
意思就是在多个机器上启动多个Rabbit MQ实例,每个机器启动一个,但是你创建的queue只会放在一个Rabbit MQ实例上,但是每个实例都去同步queue的元数据。这样在你消费的时候实际上如果连接到了另外一个实例,那么这个实例会去queue所在的实例上将数据拉取过来。这种方式很麻烦并且没有做到所谓的分布式,就是普通的集群。这样会导致要么消费者每次随机连接一个实例然后拉取数据,要么就固定连接那个queue的实例消费数据,前者有数据拉取的开销,后者导致单实例的瓶颈。
- 优点:
- 这种模式只是提高了消费者消费的吞吐量
- 缺点:2. 可能会在Rabbit MQ集群内部产生大量的数据传输3. 可用性没有什么保障,如果queue所在的节点宕机了,数据就丢失了,因为那个queue所在的实例包含元数据和实际数据
镜像集群模式(非分布式高可用):
这种模式才是所谓的Rabbit MQ真正的高可用模式,与普通集群模式不同的是:你创建的queue无论是元数据还是queue的消息会存在于多个实例上,每次写消息到queue的时候,都会自动把消息与多个实例的queue进行消息同步。
- 优点:
- 其中某个机器宕机了,别的机器还可以继续提供服务。
- 缺点:
- 这个性能相比较而言开销较大,消息需要同步所有消息。导致网络带宽压力和消耗很重。
- 还有一点就是所谓的扩展性几乎没有,因为假设某个queue的数据负载很重,加机器无法线性去扩展queue。
如何开启Rabbit MQ的镜像模式
在管理控制台新增一个镜像集群的策略,要求所有节点同步数据。
Kafka的高可用性
- 基本认识:
Kafka是多个broker组成,每个broker是一个节点,你创建一个topic,这个topic可以划分为多个partition,每个partition可以存在不同的broker上,每个partition就放一部分数据。天然的分布式消息队列。因为一个topic数据是分散在多个机器上的。每个机器之存放一部分数据。Tip:
Kafka0.8之前是没有HA(高可用)机制的。就是任何一个broker宕机了,那么这个broker上的partition就废了,没法写也没有办法读。Kafka0.8以后,提供了HA机制,就是replica副本机制,每个partition的数据都会同步到其它机器上,形成自己的多个replica副本。然后所有replica会选举出一个leader出来,那么生产和消费都和这个leader打交道,然后其它的replica就是follower。写的时候leader只负责把数据同步到follwer上,读的时候直接读leader。如果leader宕机follwer会变成leader,follwer变成leader过程无法提供对外服务。
如何保证消息不被重复消费?(如何保证消息消费时的幂等性?)
Kafka(消费者offset没来得及提交导致重复消费)
- 生成者不重复发送消息到MQ
mq内部可以为每条消息生成一个全局唯一、与业务无关的消息id,当mq接收到消息时,会先根据该id判断消息是否重复发送,mq再决定是否接收该消息。
- 消费者不重复消费
消费者怎么保证不重复消费的关键在于消费者端做控制,因为MQ不能保证不重复发送消息,所以应该在消费者端控制:即使MQ重复发送了消息,消费者拿到了消息之后,要判断是否已经消费过,如果已经消费,直接丢弃。所以根据实际业务情况,有下面几种方式:
- 如果从MQ拿到数据是要存到数据库,那么可以根据数据创建唯一约束,这样的话,同样的数据从MQ发送过来之后,当插入数据库的时候,会报违反唯一约束,不会插入成功的。(或者可以先查一次,是否在数据库中已经保存了,如果能查到,那就直接丢弃就好了)。
- 让生产者发送消息时,每条消息加一个全局的唯一id,然后消费时,将该id保存到redis里面。消费时先去redis里面查一下有么有,没有再消费。(其实原理跟第一点差不多)。
- 如果拿到的数据是直接放到redis的set中的话,那就不用考虑了,因为set集合就是自动有去重的。
如何保证消息的可靠性传输(如何处理消息丢失的问题?)
主要分为三种:
- 生产者丢失
- MQ自己丢失了
- 消费的时候丢了
Rabbit MQ消息丢失的情况及如何处理
image.png
生产者弄丢了消息
写消息的过程中,消息都没到Rabbit MQ,在网络传输过程中就丢了;或者是消息到了Rabbit MQ但是MQ内部错乱没有存下来导致消息丢失。
- 方案1:可以使用Rabbit MQ事务机制如下:
channel.txSelect();
try{
// 发送消息
}catch{
channel.txRollback();
}
channel.txCommit();
弊端:是事务机制,同步阻塞的,会导致生产者发送消息的吞吐量大大下降。
- 方案2:把channel设置成comfirm模式,发送一个消息就不用管了,Rabbit MQ如果接收到了这个消息就会回调生产者本地的一个接口,通知你说这条消息已经发送成功并且接收成功。反之也会通知。
该方式的吞吐量会高一些
Rabbit MQ本弄丢了消息
将Rabbit MQ设置成持久化的。除非有极其罕见的情况Rabbit MQ还没来得及持久化自己就挂了,可能回导致少量的数据丢失。这种概率很小。设置持久化的步骤(必须两个同时设置):
- 创建queue的时候将其设置为持久化的,这样保证Rabbit MQ持久化queue的元数据,但是不会持久化queue里的数据
- 发送消息的时候将消息的deliveryMode设置为2.就是将消息设置为持久化。此时Rabbit MQ就会将消息持久化到磁盘上去。
消费者弄丢了消息
只有当你打开了消费者的autoAck
的这样一个机制;你消费到了数据之后消费者会自动通知Rabbit MQ说我已经消费到了这条数据;这样会出现一种情况就是假设消费到了一条数据还没处理完,此时消费者就自动autoAck
了,此时恰巧消费者系统服务挂掉了,消息还没处理完而且Rabbit MQ以为该消息已经处理掉了。
- 方案:关闭掉Rabbit MQ的自动ACK机制。
Kafka消息丢失的情况
消费者弄丢了消息
消费者自动提交了offset,其实消息还没有处理完。和Rabbit MQ情况差不多。
- 解决方案:关闭自动提交offset,手动提交offset。
Kafka弄丢了消息
Kafka的leader接收到了消息但是还没来得及同步给follwer就挂掉了,此时follwer变成了leader,导致数据丢失了
- 解决方案:设置4个参数
- 给topic设置
replication.replicas
参数,这个值必须大于1,也就是要求每个partition至少有两个副本 - 在Kafka服务端设置
min.insync.replicas
参数:这个值必须大于1,这个是要求一个leader至少感知到至少有一个follwer还跟自己保持联系,这样才能确保leader还有一个follwer。 - 在producer端设置
ack=all
:这个是要求每条数据必须是写入所有的replica之后,才能认为是成功了 - 在producer端设置
retries=MAX
:这个是要求一旦写入失败,就无限重试,卡在这里。
按照上述配置后至少在Kafka端就可以保证在leader所在的broker发生故障,进行leader切换时,数据不会丢失。
生产者会不会丢失数据
如果按照上述方式设置了ack=all
一定不会丢,要求是:你的leader接收到消息,所有的follower都同步到了消息之后,才认为本次消息发送成功,否则生产者会重试无限次。
7. 如何保证消息的顺序性
假设做一个MySQL binlog同步系统,你在MySQL里增删改一个条数据,对应出来增删改3条binlog,接着将这三条binlog发送到MQ里面,到消费出来一次执行,需要保证消息的顺序性。不然数据就会出现问题。
不同MQ错乱的场景:
Rabbit MQ
- Rabbit MQ:一个queue,多个consumer,这就会出现问题;因为多个消费者是同步一起执行的,无法保证顺序,并且也无法保证消费者消费到了哪条数据。
image.png
解决方案
- 解决方案:每个消费者建立对应的queue,并且让保持顺序的消息只发送到一个queue上,这样消费者消费数据处理的时候就不会出现顺序错乱。
image.png
Kafka
Kafka可以保证生产者写入一个partition的数据一定是有顺序的。
如何保证数据写入一个partition中去:
生产者在写入数据的时候可以指定一个key,比如指定某个订单id作为key,这个订单相关的数据就会被分发到一个partition中去。Kafka有一个原则是一个partition只能被一个消费者消费
消费者从partition中取出来数据的时候,一定是有顺序的。
什么情况下Kafka会出现消息顺序不一致呢?
消费者内部搞多个线程并发处理,则可能会出现顺序不一致的问题。
解决方案
按照hash算法
进行hash分发。相同的订单key的数据分发到同一个内存queue
里面。如图
如何解决消息延时过期失效的问题
Rabbit MQ有一个TTL过期时间。关掉不要开启TTL
如何解决消息积压的问题
Rabbit MQ消息积压
- 解决思路:
临时紧急扩容。具体操作如下:
由于消费者故障的解决方案
- 如果consumer有问题,先修复consumer的问题,确保其恢复消费速度。然后将现有consumer都停掉。
- 临时建立好原先10倍或者20被queue的数量。(Kafka-新建一个topic,partition是原来的10倍。)
- 写一个临时分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时处理,直接均匀轮询写入已经建立好10倍/20倍数量的queue。
- 接着临时征用10倍的机器来部署consumer,每一批consumer来消费一个临时queue的数据。
- 等快速消费完积压的数据之后,得恢复原来部署架构,重新用原来的consumer机器来消费