当前位置: 首页 > 科技观察

这波消息中间件面试90%的Java程序员都扛不住!

时间:2023-03-19 18:21:59 科技观察

本文经授权转载自公众号:狮山的架构笔记概述你平时会用到一些消息中间件(MQ),但你对它的理解可能仅仅停留在使用API??实现消息的生产和消费。向上。关于MQ更深入的问题,可能很多人都没有想太多。比如当你换工作去面试的时候,如果面试官看到你在简历上写了,并且精通消息中间件,那么很可能会为你发起以下4种面试连环炮!为什么要使用MQ?使用MQ后有什么优缺点?如何保证MQ消息不丢失?如何保证MQ的高可用?本文将通过一些场景,以通俗易懂的语言和多张手绘彩图来探讨这些问题。为什么要使用MQ?相信大家都听过这样一句话:好的架构不是设计出来的,而是演化出来的。这句话同样适用于引入MQ的场景。使用MQ肯定是有原因的,就是用来解决实际问题的。没看到别人用,自己也用了一段时间。其实使用MQ的场景还是蛮多的,但是最核心的有3个:异步、解耦、异步削峰填谷。我们通过一个实际案例来说明:假设A系统收到一个请求,需要在本地写一个数据库来执行SQL,那么需要调用BCD三个系统的接口。假设写入本地库需要3ms,调用三个BCD系统分别需要300ms、450ms、200ms。那么最终总的请求延迟是3+300+450+200=953ms,接近1s,用户可能会觉得太慢了。这时候整个系统大概是这样的:但是一旦使用了MQ,系统A只需要向MQ中的3个消息队列发送3条消息,然后返回给用户即可。假设向MQ发送消息需要20ms,那么用户感知这个接口只有20+3=23ms,用户几乎没有感知,非常爽!这时候整个系统结构大概是这样的:可以看到,通过MQ的异步功能,可以极大的提升接口的性能。解耦假设A系统需要在用户执行某项操作时,将用户提交的数据同时推送给B系统和C系统。这时候负责A系统的哥们就想:没关系,B和C这两个系统给我提供了一个Http接口或者RPC接口,我给它推送数据就完了。负责系统A的小伙伴很开心。如下图:看起来一切都很好,但是随着业务快速迭代,这个时候系统D也想要这个数据。这样的话系统A的开发同学应该改一下,在向BC发送数据的时候加一个D。然而,越到后面,我越发现麻烦来了。..整个系统似乎不仅将这个数据发送到BCD,而且还将第二个和第三个数据发送到BCD。甚至有时候加了E、F等系统,他们也想要这个数据。而且有时候B系统可能突然不需要这个数据了,A系统应该改一下,A系统的开发小伙伴都麻木了。更复杂的场景是,通过接口向其他系统传输数据时,有时需要考虑一些重试、超时等异常情况,真是灰头土脸。..看下图,感受下无奈的一幕:这时候,我们的MQ就该登场了!这种情况下用MQ解耦比较合适,因为负责系统A的小伙伴只需要把消息丢到MQ上就行,其他系统可以按需订阅消息。即使某个系统不需要这个数据,也不会要求系统A改代码。看看下面这张与MQ解耦的图,是不是清爽了很多!比如我们的订单系统,在下订单的时候,数据会写入数据库。但是数据库每秒只能支持1000左右的并发写入,再高的并发也很容易宕机。低峰时期,并发数只有100多,但是到了高峰期,并发数会突然增加到5000多,这个时候,数据库肯定是死了。如下图,感受一下数据库被活活打死的绝望:但是用了MQ之后,情况就变了!消息由MQ保存,然后系统可以根据自己的消费能力进行消费,比如每秒1000条数据,这样慢慢写到数据库中,这样数据库就不会被kill掉:整个过程,如如下图所示:至于为什么叫削峰填谷?看这张图:如果不使用MQ,并发的高峰高峰期有一个“峰”,高峰期过后有一个低并发的“谷”。但是使用了MQ之后,消费消息的速度被限制在1000条,但是这样一来,高峰期产生的数据必然会积压在MQ中,高峰期就会被“砍”。但是由于消息的积压,在高峰期过后的一段时间内,消费消息的速度会保持在1000QPS,直到将积压的消息消费完,这就是所谓的“填谷”。通过上面的分析,大家可以知道为什么要使用MQ,使用MQ有什么好处。知道为什么,理解为什么你的系统使用MQ。这样以后别人问你为什么用MQ的时候,就不会出现“我们组长要用MQ,我们就用”这样尴尬的回答。使用MQ后有什么优缺点?看到这道题就糊涂了,就用吧!上面说了优点,那么缺点是什么呢?好像没有什么缺点。如果你这样想,那你就大错特错了。在设计系统的过程中,除了要清楚知道为什么要用这个东西,还要想一想使用它的弊端。只有这样,我们才能心中有底,防患于未然。接下来我们来讨论一下,使用MQ有什么缺点?系统可用性降低。想一想,上面提到的解耦场景。本来A系统的哥们想把系统的关键数据发给BC系统,现在突然加入一个MQ已经建立了,现在BC系统接收的数据必须要通过MQ接收。但是大家有没有考虑过一个问题,如果MQ挂了怎么办?这就引出了一个问题,加入MQ之后,系统的可用性会不会降低呢?因为还有一个额外的风险因素:MQ可能会挂掉。只要MQ挂了,数据就没了,系统就不能正常工作。系统的复杂性增加了。本来我的系统通过接口调用一次就可以搞定,但是加了MQ之后,要考虑消息重复消费,消息丢失,甚至消息顺序的问题。为了解决这些问题,需要引入许多复杂的机制,这样会不会增加系统的复杂度?数据一致性的问题很好。A系统调用BC系统接口。如果BC系统出错,会抛出异常返回给A系统,让A系统知道。这样就可以进行回滚操作了。但是使用了MQ之后,系统A发送完消息后,就完了,也算是成功了。而且恰好C系统写数据库失败了,A却认为C成功了?这样一来,数据就不一致了。分析完引入MQ的优缺点,你会明白使用MQ有很多优点,但是你会发现它带来的缺点需要你做各种额外的系统设计来弥补***你可能会发现整个系统复杂了好几倍,所以在设计系统的时候,你要根据这些考虑做出取舍,很多时候你会发现还是要用的。..如何保证MQ消息不丢失?使用MQ之后,你还应该关注消息丢失的问题。这里我们选择RabbitMQ来说明。生产者丢失数据。RabbitMQ生产者向rabbitmq发送数据时,可能会在网络传输过程中丢失数据。此时RabbitMQ收不到消息,消息丢失。RabbitMQ提供了两种方式来解决这个问题:事务模式:在生产者发送消息之前,通过`channel.txSelect`启动事务,然后发送消息。如果RabbitMQ没有成功接收消息,生产者将收到异常。此时可以回滚channel.txRollback交易,然后重新发送。如果RabbitMQ收到此消息,它可以提交事务channel.txCommit。但是这样一来,producer的吞吐量和性能都会大大降低,现在一般不会这样做。另一种方式是通过confirm机制:这种confirm方式设置在生产者所在的地方,即每次写入消息都会分配一个唯一的id,然后RabbitMQ收到后返回一个ack告诉生产者这条消息好的。如果rabbitmq没有处理消息,会回调一个nack接口,producer此时可以重新发送。事务机制和cnofirm机制最大的区别在于事务机制是同步的。提交交易后会阻塞在那里,但是确认机制是异步的。发送一条消息后,可以发送下一条消息,然后由rabbitmq接收消息。会异步回调一个接口,通知你消息已经收到。所以producer一般会使用confirm机制来避免数据丢失。如果Rabbitmq丢失了数据,那么RabbitMQ集群也会丢失消息。官方文档的教程中也提到了这个问题。也就是说,消息发送到RabbitMQ后,默认是没有登陆盘的。万一RabbitMQ宕机了,这个消息就丢失了。所以为了解决这个问题,RabbitMQ提供了持久化机制。消息写入后会持久化到磁盘,这样即使宕机,恢复后也会自动恢复之前存储的数据。这种机制可以保证消息不会丢失。设置持久化有两个步骤:首先是在创建队列的时候设置为持久化,这样rabbitmq可以保证队列的元数据被持久化,但是队列中的数据不会被持久化。二是发送消息时,设置消息的deliveryMode为2,即设置消息为持久化,然后rabbitmq会将消息持久化到磁盘。但是这样一来,可能有人会说:消息发到RabbitMQ还没来得及持久化到磁盘就挂了,数据丢失了怎么办?对于这个问题,其实是结合了上面的confirm机制。保证在消息持久化到磁盘之前不会向生产者发送ack消息。遇到这种极端情况,制作方是可以感知的。此时生产者可以通过重试向其他RabbitMQ节点发送消息。消费者丢失数据。RabbitMQ消费者丢失数据。是这样的:消费消息的时候,刚拿到消息,进程却挂了。这个时候RabbitMQ就会认为你消费成功了,这个数据就丢失了。对于这个问题,首先要解释一下RabbitMQ消费消息的机制:当消费者收到消息后,会向RabbitMQ发送一个ack,告诉RabbitMQ消息已经被消费,这样RabbitMQ就会删除这条消息。但是默认情况下发送ack的操作是自动提交的,也就是说consumer一收到这条消息就会自动返回ack给RabbitMQ,所以会存在消息丢失的问题。所以这个问题的解决方法是:关闭RabbitMQ消费者自动提交ack,消费者处理完消息后手动提交ack。这样即使遇到上面的情况,RabbitMQ也不会删除这条消息,会在你的程序重启后重新发送这条消息。如何保证MQ的高可用?使用MQ之后,我们肯定希望MQ具备高可用特性,因为无法接受机器宕机无法收发消息的情况。本篇也是基于RabbitMQ等经典MQ来讲解:RabbitMQ比较有代表性,因为它是基于主从高可用的,我们就以他为例来讲解如何实现第一个MQ的高可用.Rabbitmq有三种模式:单机模式、普通集群模式、镜像集群模式单机模式单机模式是demo级别,也就是说只有一台机器部署一个RabbitMQ程序。这样就会有单点问题,宕机就完了,就没有高可用可言了。一般都是本地启动玩玩的,没有人用单机模式做生产。普通集群模式这种模式是指在多台机器上启动多个rabbitmq实例。类似于主从模式。但是创建的队列只会放在一个masterrabbitimq实例上,其他实例会同步接收消息的RabbitMQ元数据。在消费消息时,如果你连接的RabbitMQ实例不是存储Queue数据的实例,此时RabbitMQ会从存储Queue数据的实例中拉取数据,然后返回给客户端。总的来说,这种方法有点繁琐,也不是真正意义上的分布式。每次消费者连接到一个实例时,它都会拉取数据。如果它连接到一个不存储队列数据的实例,此时会造成额外的性能开销。如果从Queue上的实例中拉取,会造成单实例性能瓶颈。如果队列上的实例宕机,其他实例将无法拉取数据,集群将无法消费消息,没有达到真正的高可用。那么这个事情就比较尴尬了,根本就没有所谓的高可用,这个方案主要是为了提高吞吐量,也就是说让集群中的多个节点服务于某个队列的读写操作。镜像集群模式镜像集群模式是rabbitmq真正的高可用模式。它不同于普通的集群模式:无论元数据或队列中的消息如何,创建的队列将存在于多个实例中。每次向队列中写入一条消息时,该消息会自动发送到多个实例的队列中进行消息同步。这样,如果任何一台机器宕机,都可以使用其他实例提供服务,从而实现真正的高可用。但也有缺点:性能开销过高,消息需要与所有机器同步,会导致网络带宽压力大,消耗大。可扩展性低:无法解决某个队列的数据量特别大,导致队列无法线性扩展的情况。即使增加一台机器,那台机器也会包含队列的所有数据,而队列的数据并不是分布式存储的。对于RabbitMQ的高可用,一般的做法是开启镜像集群模式,这样至少做到了高可用。如果一个节点宕机,其他节点可以继续提供服务。总结通过本文,分析了一些关于MQ的常见问题:为什么要使用MQ?使用MQ的优点和缺点是什么?如何保证消息不丢失?如何保证MQ的高可用?但是,这些问题只是使用MQ需要考虑的一部分,其实还有其他更复杂的问题需要我们解决,比如:如何保证消息的顺序?如何选择消息队列?如何解决消息积压问题?本文只是针对RabbitMQ场景的示例。还有其他类似的消息队列,例如RocketMQ和Kafka。当不同的MQ面临上述问题时,需要按照各自的原则和机制进行处理。这些不在本文考虑范围内,将在以后的文章中讨论。敬请关注。

猜你喜欢