1.什么是消息中间件?
消息是指应用间传输的数据。消息体包括文本字符串、Json、内嵌对象等。
消息中间件是基于队列模型实现异步和同步传输数据的。
作用:解耦,冗余(存储)、扩展性、削峰、可恢复性、顺序保证、缓冲、异步通信。通俗点来说就是支持支撑高并发、异步解耦、流量削峰、降低耦合度。
2. AMQP是什么?
AMQP(Advanced Message Queuing Protocol,高级消息队列协议)是一个进程间传递异步消息的网络协议。
RabbitMQ是遵从AMQP协议的,而RabbitMQ的模型架构又和AMQP的模型架构是一样的。
AMQP实体模型如下:
工作原理如下:
生产者(Publisher)将消息发送给交换器,交换器和队列绑定。当生产者发送消息时所携带的RoutingKey与绑定时的BindingKey相匹配时,消息即被存入相应的队列之中,消费者可以订阅相应的队列来获取消息。
3.RabbitMQ相关概念
RabbitMQ实体模型如下:
在了解完上图RabbitMQ模型后,现在让我们来看看RabbitMQ运转流程:
●生产者推送消息:
(1)生产者连接到RabbitMQ的Broker建立一个连接(Connection),开启一个信道(Channel)。
(2)生产者声明一个交换器,并设置相关属性,比如交换机类型、是否持久化等。
(3)生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除等。
(4)生产者通过路由键将交换器和队列绑定起来。
(5)生产者发送消息至RabbitMQ的Broker,其中包含路由键、交换器等信息。
(6)相应的交换器根据接收到的路由键查找相匹配的队列。
(7)如果找到,则将从生产者发送过来的消息存入相应的队列中。
(8)如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者。
(9)关闭信道。
(10)关闭连接。
●消费者接收消息:
(1)消费者连接到RabbitMQ的Broker,建立一个连接(Connection),开启一个信道(Channel)。
(2)消费者向RabbitMQ的Broker请求消费相应队列中的消息,可能会设置相应的回调函数,以及做些准备工作(推拉模式)。
(3)等待RabbitMQ的Broker回应并投递相应队列中的消息,消费者接收消息。
(4)消费者确认(ack)接收到的消息。
(5) RabbitMQ从队列中删除相应己经被确认的消息。
(6)关闭信道。
(7)关闭连接。
注:上述有两个概念可以了解下,分别是信道(Channel)与连接(Connection),我们知道无论是生产者还是消费者,都需要和RabbitMQ的Broker建立连接,这个连接就是一条TCP连接,也就是Connection的TCP连接建立起来,客户端紧接着可以创建一个AMQP信道(Channel),每个信道都会被指派一个唯一的ID。Channel是建立在Connection之上的虚拟连接,RabbitMQ处理的每条AMQP指令都是通过信道完成的。如图所示:
*这里大伙可能会有一个疑问,为什么Connection能完成Channel连接工作,还要引入Channel呢?
试想这样一个场景,一个应用程序中有很多个线程需要从RabbitMQ中消费消息或者生产消息,那么必然需要建立很多个Connection,也就是许多个TCP连接。然而对于操作系统而言,建立和销毁TCP连接是非常昂贵的开销,如果遇到使用高峰,性能瓶颈也随之显现。RabbitMQ采用类似NIO1(Non-blocking 1/0)的做法,选择TCP连接复用,不仅可以减少性能开销,同时也便于管理。
*NIO1,也称非阻塞(I/O),包含三大核心部分Channel(信道)、Buffer(缓冲区)和Selector(选择器)。NIO基于channel和Buffer进行操作,总是从信道读取数据到缓冲区中,或者从缓冲区写入到信道中。Selector用于监听多个信道的事件(比如连接打开,数据到达等)。因此,单线程可以监听多个数据的信道。NIO1中有个很有名的Reactor模式,有兴趣小伙伴可以去阅读下,这里就不详解了。
*每个线程分别把持一个信道,所以信道复用了Connection TCP连接。同时RabbitMQ可以确保每个线程的私密性,就像拥有独立的连接一样。当每个信道的流量不是很大时,复用单一的Connection可以在产生性能瓶颈的情况下有效地节TCP连接资源。但是当信道本身的流量很大时,这时候多个信道复用一个Connection就会产生性能瓶颈,进而使整体的流量被限制了。此时就需要开辟多Connection,将这些信道均摊到这些Connection中,至于这些相关的调优策略需要根据业务自身的实际情况进行调节。
下面让我们来了解下实体中每个角色模型。
●Producer(生产者):投递消息到服务端的一方。投递消息分为两部分:消息体(payload)和标签(label)。
◎消息体:一般是带有业务逻辑结构的数据,例如一个json数据。
◎标签:消息的标签是用来表述这条消息,例如一个交换器名称跟路由键。
●Broker(代理):MQ服务节点。也可以看作一个实例,或者一台RabbitMQ服务器。
●Exchange(交换器):生产者将消息投递到队列中,实际是将消息投递到交换器,由其路由到一个或者多个队列当中。如果路由不到,会返回给生产者或者丢弃。交换器包含路由键(RoutingKey)和绑定键(BindingKey),还有四种类型,后面会重点介绍。
◎路由键:生产者投递消息给交换器时会指定一个路由键,用作路由规则来决定消息的流向。
◎绑定键:交换器跟队列是通过一个绑定键关联的,这样Rabbitmq才能正确把消息投递到相关队列去。如图所示:
从上图可知道,交换器相当于投递包裹的邮箱,路由键当于填写在包裹上的地址,绑定键当于包裹目的地,当填写在包裹上地址和实际想要投递的地相匹配时,那么这个包裹才会被正确投递到目的地,最后这个目的地的“收件人”(队列)才可以保留这个包裹。如果填写的地址有错,邮递员不能正确投递到目的地,那么包裹可能会回退给“寄件人”(生产者),或者可能会被丢弃。其实在某些情形下路由键和绑定键可以看作同一个东西。
●Queue(队列):用于存储消息。遵循先进先出,后进后出原则。多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin),即轮询给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
4.为什么要用到MQ?
4.1传统Http请求缺点
●传统Http请求是基于请求与响应的模型下进行的,在高并发的情况下,客户端会发送大量的请求达到服务器端,这样有可能会导致我们服务器端要处理大量堆积请求,增加服务器压力。
●像Window IIS服务器(IIS线程池)处理每个请求都有自己独立的线程,如果超过最大线程数,IIS的w3wp进程不会再将http.sys的队列中的请求去线程池中调用线程处理,如果请求堆积过多的情况下,有可能会导致服务器崩溃。所以一般都会在nginx入口实现限流。
●http请求处理比较耗时的业务逻辑情况下,容易造成客户端(Client)一直等待,阻塞等待过程中会导致客户端超时发生重试策略,有可能会引发幂等性问题。
总结:综合以上Http请求缺点,如果接口是为Http协议的情况下,最好不要处理比较耗时的业务逻辑,耗时的业务逻辑应该单独交给多线程或者是mq去处理。
4.2MQ应用场景
结合上述总结可以知道,在处理Http请求比较耗时业务时,可以交给多线程或者是mq去处理,下面我们结合常见用户注册应用场景来解释这两者区别:
如上图所示,客户端发起一个注册用户请求,处理业务逻辑如下:
(1)InsertUser(插入用户信息),需要1秒处理时间。
(2)SendEmail(发送邮件),需要2秒处理时间。
(3)SendCoupons(发送优惠卷),需要3秒处理时间。
按照上述串行处理流程,该功能处理时间总共为6秒,这意味着用户需要等待6秒,这种用户体验是非常差的。下面再来看看多线程跟MQ如何处理这些业务逻辑。
●多线程处理
如上图所示,客户端发起一个注册用户请求到服务端后,插入用户信息同时开启一个线程处理发送邮件和优惠卷,当插入用户信息成功后立即响应状态给客户端,所以整个过程只花费1秒处理时间。
◎优点:适合小型项目,实现异步处理。
◎缺点:消耗服务器CPU资源,没有解耦。
●MQ处理
如上图所示,客户端发起一个注册用户请求到服务端后,插入用户信息同时往MQ投递一个消息处理发送邮件和优惠卷,当插入用户信息成功后立即响应给客户端,所以整个过程只花费1秒处理时间。
◎优点:适合中大型项目,实现异步处理,流量削峰、降低耦合度。
◎缺点:增加系统复杂度。
5.主流MQ区别对比
特性 |
ActiveMQ |
RabbitMQ |
RocketMQ |
Kafka |
开发语言 |
java |
erlang |
java |
scala |
单机吞吐量 |
万级 |
万级 |
10万级 |
10万级 |
时效性 |
ms级 |
us级 |
ms级 |
ms级以内 |
可用性 |
高(主从架构) |
高(主从架构) |
非常高(分布式架构) |
非常高(分布式架构) |
功能特性 |
成熟的产品,在很多公司得到应用,有较多的文档,各种协议支持较好。 |
基于erlang开发,所以并发能力很强,性能极其好,延时很低管理界面较丰富。 |
MQ功能比较完备,扩展性佳。 |
只支持主要的MQ功能,像一些消息查询,消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。 |
参考文献:
RabbitMQ实战指南