MQ,MessageQuene即 消息队列,是程序与程序之间的异步通信
一种方法。
为什么使用MQ?
MQ用来做程序间异步通信的,为什么使用MQ问题也是为什么要引入程序间异步通讯问题。
什么是同步通讯?
同步通讯简单理解就是下一行要执行的代码要等到上一行完全执行
完毕才能够执行。例如:我要根据订单Id查询到用户信息并返回。此时就要先通过id调用订单系统查询到订单信息,在从订单信息中获取用户id,调用用户系统传递用户id去查询到用户信息并返回。
要想查询到用户信息就一定先查到订单信息,下一行代码执行依赖于上一行代码的执行结果,我们通常在代码中使用到就是同步通讯,从上到下是一步步执行的,不可以跳跃。就像当给axios加上async一样。
一个使用同步通讯不合理的例子:下单支付后,下单系统会等待支付完成,再调用仓储系统,进行扣除库存等操作,等到仓储系统全部执行成功后,在交给物流系统,等到物流系统全部执行成功后,再返回给用户下单成功信息。
同步通讯:各个系统像链一样执行,有一处断则全断,添加、删除就要修改
异步通讯:下一行代码不必等待上一行代码的执行结果,只要确保其被调用,最终会执行即可。类似于子线程、axios等
通过此例子中同步和异步通讯对比,就可以发现同步带来的问题?
性能和吞吐能力下降
:用户要等到这些系统一个个全部按顺序全部执行成功后,才能得到响应结果。使得用户的体验,大大的下降。就像去饭店点餐一样,只要顾客说出要吃的菜后,服务员就可以告诉顾客点菜成功(虽然没有做,但可以一定能够成功),而不是没有反应等到菜端上来后,菜告诉用户点菜成功。这样就会使得用户感觉点菜花的时间过长。回到上一个例子:在用户已经支付后,就已经能够确保下单一定成功了,返回给用户响应信息。其余的操作交给后台就可以了,只要保证成功即可,而不必让用户去等待。
资源浪费
:在用户支付成功后,不仅没有及时的返回用户结果,此线程还要参与调用其他系统,造成此线程资源的浪费。
耦合度高
:如果在仓储系统与物流系统中加上一个短信系统,那么这个下单系统的代码就要发生修改,在整个代码结果中塞进去一个短信系统。而使用异步通讯,只需要将一个独立的短信系统开发后,接入中间件,监听来自下单系统的统一发送的通知即可。
调用者只负责将信息发送到队列中,被调用者只负责从消息队列中取出消息进行调用,二者通过这个中间人(消息队列)实现代码解耦。
级联失败
:同步通讯中,在此过程如果发生一个错误,那么下面代码就不会执行,整个业务就全发生错误。但使用异步通讯,业务之间都是通过中间件获取消息的,彼此之间没有联系。即使有一个业务执行失败了,也不会影响到其他业务。
总结:
使用异步通讯,能够快速的对用户操作做出反应(虽然没有执行完,但能确保最终执行成功),释放掉资源用给下一个用户。异步服务间相对独立,都统一受中间件的调用,要添加,删除某个服务十分的方便,同时,当有服务发生故障时,也不会影响到其他服务。
既然异步通信这么好,为什么还要使用同步通讯?:
还是上一个例子,下单系统中一定要同步调用支付系统,因为后续的调用都在等在支付结果。如果要使用异步通讯,最后还要在代码的下面等待结果,就可有能还不如同步通讯调用这样有更高的时效性
。
怎样实现异步通讯?
如果是一个服务,可以开启一个子线程去调用即可,但这样只能使得要用户的响应速度得到提升。那如果是很多个服务呢?服务动态伸缩呢?这就需要一个消息队列。
通常就是将调用的消息发送到一个队列中,各个被调用者各自监听这个队列中的消息,队列中一有消息,就从消息队列中获取参数执行对应的方法。
解耦
:服务调用之间可以达到插拔式效果,各个服务间彼此隔离,互不影响
异步
:无需等待后续处理,提升了系统吞吐量,提升响应速度,减少阻塞
削峰
:大流量由消息队列先挡在各个服务的前面,消息可以在队列中留存作为缓冲,队列以合适的速度去调用
就像一个水坝一样,当有大的洪水时,水坝可以将洪水留在蓄水池中,然后以合适的速度下放,起到一个缓冲的作用。
使用MQ的坏处:
注意:不是MQ去调用各个服务的,是各个服务监听MQ的消息通过回调去调用对应的方法
思考:
常见的MQ有Rabbit MQ
,Rocket MQ
,Active MQ
,Kafka
。这些技术里Active MQ较弱
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang |
Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 |
一般 | 高 |
高 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 |
毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 |
一般 | 高 |
一般 |
RabbitMQ和RocketMQ是消息延迟
与吞吐量
的取舍,kafka最大亮点高吞吐
,通常都是在大数据中使用的。
使用命令
docker run \
-e RABBITMQ_DEFAULT_USER=用户名 \
-e RABBITMQ_DEFAULT_PASS=密码 \
--name mq \
--hostname myhost \
-p 15672:15672 \
-p 5672:5672 \
-d \
Rabbit镜像id
RabbitMQ暴露两个端口,15672是用来供浏览器访问提供可视化管理的,5672是用来被各个服务连接的。
通过浏览器访问15672,输入配置的用户名,密码即可来到管理页面
管理面板有很多的功能,不止可以用于查看,例如:向队列中添加消息,查看队列消息,添加队列,管理队列,添加交换机,管理交换机等。
MQ最基本的就是图中四大个角色
publisher
:消息发布者,就是各个服务的调用者,就是它将消息发送到消息队列,其他服务(下面的消费者)监听回调
exchange
:交换机,负责将消息广播到各个队列。给我的感觉就像凹透镜发散光一样,将消息发布者的消息广播到订阅的队列中,注意是同一个消息送到指定队列。如果发布者只将消息发送到一个队列中,则无需交换机。有交换机就与队列存在发布与订阅关系。交换机不能缓存数据,发送到队列过程失败,消息直接丢失。
quene
:消息队列,有推送消息到消费者和暂存消息的功能。队列中的消息只能被一个消费者消费。
consumer
:消息消费者,消费者一定是要从队列中获取消息的,然后在其内部监听队列中消息,监听的队列中一有消息就进行回调,达到调用服务中的方法目的。
一个服务使用一个队列,服务中的各个实例作为一个队列下的消费者。
生产者也可以有多个,但在模型中,一个和多个只要以相同的消息格式往一个队列里发是一样的,为了简化用1个实例去代表
没有交换机:
没有交换机,适用于消费者是一个服务下有多或一个实例
,各个实例间争抢消费同一个消息。
有交换机
交换机可以将一个消息发送到各个队列中,用于将调用信息发送到各个服务对应的某个实例下
但那些队列能够获取交换机的消息呢?常见的有以下三种
主题 Topic
:只有匹配对应上的队列才能够接收到消息,与上一个不同的是,匹配的不要求一一对应,可以是一个范围,一个.
级别
思考:当一个RabbitMQ服务器中不止这一组服务发布者与消费者怎么办?也就是说,多个相同的不同业务队列模型怎样进行隔离呢?
RabbitMQ中引入了一个VirtualHost
,通过这个达到多租户
效果,即一个RabbitMQ在逻辑上被分为多个小RabbitMQ,VirtualHost
之间做到完全隔离。
因此,在连接时不仅要指定RabbitMQ的ip和端口,还要具体到是哪一个VirtualHost
什么是AMQP :AMQP,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。
SpringAMQP 在代码中简化RabbitMQ的使用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
VirtualHost
(默认为/
)spring:
rabbitmq:
host: 192.168.83.100
port: 5672
virtual-host: /
username: yan
password: 1234
在模拟演示过程,消费者端将监听到的消息直接进行输出,在实际应用中,是根据消息作为参数利用回调调用此服务方法实现异步调用
的。
在简单消息模型中,发送一个消息需要知道 队列名字
,消费者根据队列名字
来确定监听这个队列
即一个队列,多个消费者
那么一个队列中有多个消费者,消息是如何分配的呢?一个消费者消费完消息另一个会不会再次消费呢?
例:当队列中有10个消息,有消费者A,B都监听这个队列。消费者是先将消息平均取出,再进行消费的。一个消息被取出,其他消费者就不能消费此消息。
先平均取出再进行消费就存在一个问题:,性能差的和性能好的取出的一样多,但一样数量的消息消费起来时间就不一样了,就会发生性能好的消费者空余,性能差的消费者处理不过来。整体的服务处理效率就会降低,应该让他们按处理能力去取消息。
如果使其按处理能力取出呢?
就好比吃自助餐,饭量小的和饭量大的拿的一样多,要使其按饭量去取,就要让他们吃完再去取。消费队列也是一样,每次每个消费者只能拿1个消息(可以配置个数),处理过后,再去取下一个消息,虽然取的次数变多了,但能者多劳,分配的更加合理,整体的处理效率就提高了。
修改配置:
引入了交换机向各个队列中扩散消息
消费者依然指定队列名从队列中获取消息,但与生产者直接相连的不在是队列,而是交换机。发布者向交换机中发送信息,交换机在给绑定的队列广播信息
因此在生产者端就要声明交换机,声明队列,绑定交换机与队列最后将消息发送给交换机
这里使用名字为fan1
的交换机 使用队列名为q1
,q2
的两个队列与此交换机进行绑定,每个队列下都有一个消费者。我向交换机中发送一条消息,两个队列下的消费者都会接收到消息
方式一:使用@Bean方式 将队列与交换机进行绑定
依旧是从队列中获取消息
生产者端
生产者依旧是向队列发送消息
流程总结:
方式二:使用注解的方式
生产者依旧是将消息发送给交换机,但它不必去管理队列和绑定交换机与队列
消费者在监听队列时,指明其交换机并且绑定队列与交换机的关系
交换机总结:
注意:
3. 交换机不能缓存数据,交换机向队列广播消息过程失败则消息丢失
与广播模式不同的是,并不是所有与交换机绑定的队列就一定能够获取交换机推送的消息,还要在此基础上,口令(routing Key
)与交换机当前消息
的口令(routing Key
)完全匹配
。这种模式能够使得交换机将消息广播到与它绑定的队列中,指定的某些队列上。
例如:我有在订单系统本次发送消息只想调用仓储系统,但与交换机绑定的还有物流系统,这就需要在消息中指明routingKey
,只有与之匹配的队列才能够获取,使得调用更加灵活。
使用方式在Faout模式下,生产者端在发送消息时指定routingKey
在DIRCT模式中,队列要想获取交换机中的数据满足的条件
routingKey
中包含与之绑定交换机的此次消息的routingKey
即使routingKey相同,监听队列绑定的交换机不同,也无法获取数据
Direct模式当各个队列的routingKey都含有此消息的routingKey,那么就达到了Faout模式
这种模式相较于Direct模式,routingKey可以使用通配符
。每一个.
代表隔着级别,*
代表一个级别,#
代表任意级别
例如:a.b.c
=a.*.c
=a.#
但a.b.c
!=a.*
因为*
只能代表一级(一个单词)
注意,这里*
代表的是少的,只能代表一个
这种通配符既可以在生产者的消息中指定,又可以在消费者绑定队列中指定
生产者
消费者
也可以使用@Bean的方式声明和绑定,这里只演示一个
注意:不仅可以向队列发送String类型的消息,所有类型的均支持。在发送对象作为消息时,由于对象的传输一定需要进行序列化。
那么怎样通过消息队列传递对象数据呢?
可以发送方可以将对象转为JSON字符串格式将对象序列化,消费者端可以将JSON字符串进行反序列化转为对象。
springAMQP虽然能够自动创建交换机,队列,但生产者向队列,交换机发送前前一定要确保已经存在。声明可以在消费者端,也可以在生产者端,但应该在消费者端,这样当有业务增减时对生产者可以实现无感知的。队列、交换机以及他们绑定关系的声明可以通过注解
这种方式声明,也可以通过向容器中注入bean
的方式声明
RabbitMQ中一但创建了交换机指定了类型是不允许修改的,除非删掉这个虚拟机
例如:我创建了一个交换机名为a1,指定为faout类型,但我想要将a1更换为direct,这时是不成功的,a1交换机仍为faout类型,而且会报这样的错误inequivalent arg 'type' for exchange 'e1' in vhost '/': received 'direct' but current is 'fanout'
解决方式要么换一个交换机的名字,要么删除重新创建新的类型。