一、消息中间件
在项目中,可将一些无需即时返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。
消息队列常用应用场景:
1、任务异步处理
将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。
2、应用程序解耦合
MQ
相当于一个中介,生产方通过MQ
与消费方交互,它将应用程序进行解耦合。
3、削峰填谷
如订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。低峰期的时候并发也就100多个,但是在高峰期时候,并发量会突然激增到5000以上,这个时候数据库肯定卡死了。
但是使用了MQ
之后,限制消费消息的速度为1000,但是这样一来,高峰期产生的数据势必会被积压在MQ
中,高峰就被“削”掉了。但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000QPS
,直到消费完积压的消息,这就叫做“填谷”。
二、AMQP
和 JMS
MQ
是消息通信的模型;实现MQ
的大致有两种主流方式:AMQP
、JMS
。
2.1. AMQP
AMQP
是一种协议,更准确的说是一种binary wire-level protocol
(链接协议)。这是其和JMS
的本质差别,AMQP
不从API
层进行限定,而是直接定义网络交换的数据格式。
2.2. JMS
JMS
即Java
消息服务(JavaMessage Service
)应用程序接口,是一个Java平台中关于面向消息中间件(MOM
)的API
,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
2.3. AMQP
与 JMS
区别
JMS
是定义了统一的接口,来对消息操作进行统一;AMQP
是通过规定协议来统一数据交互的格式JMS
限定了必须使用Java语言;AMQP
只是协议,不规定实现方式,因此是跨语言的。JMS
规定了两种消息模式;而AMQP
的消息模式更加丰富
2.4. 消息队列产品
市场上常见的消息队列有如下:
ActiveMQ
:基于JMS
ZeroMQ
:基于C
语言开发RabbitMQ
:基于AMQP
协议,erlang
语言开发,稳定性好RocketMQ
:基于JMS
,阿里巴巴产品Kafka
:类似MQ
的产品;分布式消息系统,高吞吐量
三、RabbitMQ
概念
RabbitMQ
官方地址:http://www.rabbitmq.com/
3.1.简介
RabbitMQ
是由erlang
语言开发,基于AMQP
(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。
RabbitMQ
提供了6种模式:简单模式,work模式,Publish/Subscribe发布与订阅模式,Routing路由模式,Topics主题模式,RPC远程调用模式(远程调用,不太算MQ
;暂不作介绍)。
3.2.核心概念
Message
消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
Exchange
有4种类型:direct
(默认),fanout
, topic
, 和headers
,不同类型的Exchange
转发消息的策略有所区别。
Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
Exchange
和Queue
的绑定可以是多对多的关系。
Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
Connection
网络连接,比如一个TCP
连接。
Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
Broker
表示消息队列服务器实体
Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost
本质上就是一个 mini
版的 RabbitMQ
服务器,拥有自己的队列、交换器、绑定和权限机制。vhost
是 AMQP
概念的基础,必须在连接时指定,RabbitMQ
默认的 vhost
是 / 。
概念 | 说明 |
---|---|
连接Connection |
一个网络连接,比如TCP/IP 套接字连接。 |
会话Session |
端点之间的命名对话。在一个会话上下文中,保证“恰好传递一次”。 |
信道Channel |
多路复用连接中的一条独立的双向数据流通道。为会话提供物理传输介质。 |
客户端Client |
AMQP 连接或者会话的发起者。AMQP 是非对称的,客户端生产和消费消息,服务器存储和路由这些消息。 |
服务节点Broker |
消息中间件的服务节点;一般情况下可以将一个RabbitMQ Broker 看作一台RabbitMQ 服务器。 |
端点 | AMQP 对话的任意一方。一个AMQP 连接包括两个端点(一个是客户端,一个是服务器)。 |
消费者Consumer |
一个从消息队列里请求消息的客户端程序。 |
生产者Producer |
一个向交换机发布消息的客户端应用程序。 |
四、Spring Boot
整合RabbitMQ
为了方便下面示例的编写,我们先直接整合Spring Boot ,不用RabbitMQ自带依赖写示例了。
4.1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
4.2.在application.yml
中添加RabbitMQ
地址
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /test
4.3.常用注解说明
@EnableRabbit
:启动注解的消息监听,需要在配置类上加上@EnableRabbit
注解。
@RabbitListener
:可以标注在方法或类上,声明这个方法是一个消费者方法,可以定义监听的队列、队列绑定关系,常用的属性:
-
queues
:指定关联容器可以侦听多个队列。 -
bindings
:指定绑定关系,可以有多个。值是@QueueBinding
的数组。@QueueBinding
包含下面属性:value
:这个消费者关联的队列。值是@Queue
,代表一个队列exchange
:队列所绑定的交换机,值是@Exchange
类型key
:队列和交换机绑定的RoutingKey
@RabbitHandler
:用在需要监听多个方法时,不同的方法接收不同的消息实体,必须能够明确区分不同的实体,否则消息不能监听成功。
@Header
、@Headers
、@Payload
:用在方法签名上,用来获取消息头信息或者明确表明消息的负载实体(也可以自动推导)。
@SendTo
:用于设定消息回复,标注的方法需要返回非空的回复实体对象
五、RabbitMQ
工作模式 五种消息模型
官网对应模式介绍:https://www.rabbitmq.com/getstarted.html
5.1.简单模式 HelloWorld
5.1.1、介绍
RabbitMQ
是一个消息代理:它接受和转发消息。 你可以把它想象成一个邮局:当你把邮件放在邮箱里时,你可以确定邮差先生最终会把邮件发送给你的收件人。 在这个比喻中,RabbitMQ
是邮政信箱,邮局和邮递员。
图例:
P(producer
/ publisher
):生产者,一个发送消息的用户应用程序。
C(consumer
):消费者,消费和接收有类似的意思,消费者是一个主要用来等待接收消息的用户应用程序
队列(红色区域):RabbitMQ
内部类似于邮箱的一个概念。虽然消息流经RabbitMQ
和你的应用程序,但是它们只能存储在队列中。队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。
总之:
生产者将消息发送到队列,消费者从队列中获取消息,队列是存储消息的缓冲区。
5.1.2、示例
编写两个程序:发送单个消息的生产者,以及接收消息并将其打印出来的消费者。
1).生产者发送消息
/******** 简单模式-声明队列 ***************/
@Bean("helloWorld")
public Queue helloWorld(){
return QueueBuilder.durable("HelloWorld").build();
}
/******** 简单模式-发送消息 ***************/
@Test
void helloWord() {
/**
* 发送消息
* 参数一:路由key
* 参数二:发送的消息
*/
rabbitTemplate.convertAndSend("HelloWorld","HelloWorld消息...");
}
2).消费者获取消息
/******** 简单模式-消费者获取消息 ***************/
@RabbitListener(queues = "HelloWorld")
public void helloWorld(String msg){
System.out.println("简单模式接收的消息:" + msg);
}
5.2.工作队列模式 Work Queue
5.2.1、介绍
工作队列或者竞争消费者模式。
工作队列,又称任务队列。主要思想就是避免执行资源密集型任务时,必须等待它执行完成。相反我们稍后完成任务,我们将任务封装为消息并将其发送到队列。 在后台运行的工作进程将获取任务并最终执行作业。当你运行许多工人时,任务将在他们之间共享,但是一个消息只能被一个消费者获取。
这个概念在Web应用程序中特别有用,因为在短的HTTP请求窗口中无法处理复杂的任务。
图例:
5.2.2、示例
接下来我们来模拟这个流程:
P
:生产者:任务的发布者
C1
:消费者,领取任务并且完成任务,假设完成速度较快
C2
:消费者2:领取任务并完成任务,假设完成速度慢
// TODO 测试
5.3.订阅模式-Publish/Subscribe
Exchange
类型有以下几种:
-
Fanout
:广播,将消息交给所有绑定到交换机的队列。 -
Direct
:定向,把消息交给符合指定routing key
的队列。 -
Topic
:通配符,把消息交给符合routing pattern
(路由模式) 的队列。
5.3.1、介绍
Publish/Subscribe
模式即Exchange
广播(Fanout
)类型,将消息交给所有绑定到交换机的队列。每个发到fanout
类型交换器的消息都会分到所有绑定的队列上去。fanout
交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout
类型转发消息是最快的。
图例:
在广播模式下,消息发送流程是这样的:
- 1) 可以有多个消费者
- 2) 每个消费者有自己的
Queue
(队列) - 3) 每个队列都要绑定到
Exchange
(交换机) - 4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
- 5) 交换机把消息发送给绑定过的所有队列
- 6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
5.3.2、示例
1).生产者发送消息
声明队列交换机:
/******** Publish/Subscribe模式 ***************/
/** 容器中的Queue、Exchange、Binding 会自动创建(在RabbitMQ)不存在的情况下
* 如果RabbitMQ有,@Bean声明属性发生变化也不会覆盖
*/
/**
* 声明交换机
* @return
*/
@Bean("fanoutExchange")
public Exchange fanoutExchange(){
return ExchangeBuilder.fanoutExchange("fanoutExchange").durable(true).build();
}
/**
* 声明队列
* @return
*/
@Bean("fanoutQueue1")
public Queue fanoutQueue1(){
return QueueBuilder.durable("fanoutQueue1").build();
}
@Bean("fanoutQueue2")
public Queue fanoutQueue2(){
return QueueBuilder.durable("fanoutQueue2").build();
}
/**
* 绑定队列和交换机
* @param queue
* @param exchange
* @return
*/
@Bean
public Binding fanoutQueue1Exchange(@Qualifier("fanoutQueue1") Queue queue,
@Qualifier("fanoutExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("fanoutQueue1").noargs();
}
@Bean
public Binding fanoutQueue2Exchange(@Qualifier("fanoutQueue2") Queue queue,
@Qualifier("fanoutExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("fanoutQueue2").noargs();
}
生产者发送消息:
@Test
void fanoutQueue() {
for (int i = 0; i < 50; i++) {
/**
* 发送消息
* 参数一:交换机名称
* 参数二:路由key:不管指定什么路由key都会转发到绑定交换机的所有队列
* 参数三:发送的消息
*/
rabbitTemplate.convertAndSend("fanoutExchange", "WorkQueue","fanoutQueue消息 ---> " + i) ;
}
}
2).消费者获取消息
/******** Publish/Subscribe-消费者获取消息 ***************/
@RabbitListener(queues = "fanoutQueue1")
public void fanoutQueue1(String msg) {
log.info("Publish/Subscribe 1模式接收的消息:" + msg);
}
@RabbitListener(queues = "fanoutQueue2")
public void fanoutQueue2(String msg) {
log.info("Publish/Subscribe 2模式接收的消息:" + msg);
}
消费者控制台打印:
2020-12-21 16:21:07.073 INFO 9460 --- [ntContainer#1-1] t.d.c.c.FanoutQueueRabbitListener : Publish/Subscribe 1模式接收的消息:"workQueue消息 ---> 37"
2020-12-21 16:21:07.073 INFO 9460 --- [ntContainer#0-1] t.d.c.c.FanoutQueueRabbitListener : Publish/Subscribe 2模式接收的消息:"workQueue消息 ---> 44"
2020-12-21 16:21:07.073 INFO 9460 --- [ntContainer#1-1] t.d.c.c.Fano