1. 消息中间件概述
1.1. 什么是消息中间件
MQ全称为Message Queue,消息队列是应用程序和应用程序之间的通信方法。多用于分布式系统之间进行通信。
为什么使用MQ:
在项目中,可将一些无需即时返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从
而提高了系统的吞吐量。
1、任务异步处理:
将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。
2、应用程序解耦合:
MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。
比如订单系统要远程调用库存系统、支付系统、物流系统,这样会耦合,修改参数时候麻烦。
使用消息队列后,订单系统给消息MQ发送一条消息就算成功了
3、削峰填谷:
如订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。
低峰期的时候并发也就100多个,但是在高峰期时候,并发量会突然激增到5000以上,这个时候数据库肯定卡住,甚至宕机。
使用mq后,在大并发下,比如每秒1000个数据写入数据库,那么就会有大量的消息数据积压在mq中,高峰就被削掉了,不会有大量请求打到数 据库,在经过高峰期后,消费消息的速度依然保持不变,那么被积压的消息就会被消费完,这叫做填谷。
1.2.消息目的地
1.队列(queue):点对点消息通信 (point-to-point)
--消息发送者发送消息,消息代理将其放入一个队列中,消息接受者从队列获取消息内容,
消息读取后被移出队列
--消息只有唯一的发送者和接收者,但并不是说只能有一个接收者(指的是消息只能被众多的消费者者 中的其中一个消费)
2.主题(topic):发布(publish) / 订阅(subscribe)消息通信
--发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时 同时接收消息
1.3.消息通信的模型
MQ是消息通信的模型;
实现MQ的大致有两种主流方式:AMQP、JMS。
JMS(java Message service)java消息服务:
--是一个Java平台中关于面向消息中间件(MOM)的API
--基于jvm消息代理的规范。ActiveMq,HornetMq是JMS实现的。
AMQO(Advanced Message Queuing Protocol):
--是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,
并不受客户端/中间件不同产品,不同的开发语言等条件的限制
--AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议),
这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式,类比HTTP。
--高级消息队列协议,也是一个消息代理的规范,兼容JMS
--RabbitMQ是AMQP的实现。
1.4.通信模型的区别
2.RabbitMQ概念
2.1.message
消息他又消息头和消息体组成。消息体是不透明的,而消息头则是由一系列的可选属性组成,这些属性包括route-key(路由键),priority(相对于其他信息的优先权),delivery-mode(指出该消息可能需要持久性存储)等
2.2.publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序
2.3.exchange
交换器,用来接收生产者发送的消息并将消息路由给服务器中的队列。
exchange有四种类型:direct(默认),fanout,topic和headers。
不同类型的exchange转发的消息的策略有所区别
2.4.queue
消息队列,用来保存消息直到发送给消费者,它是消息的容器,也是消息的终点,一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接达到这个队列将其取走。
2.5.binding
绑定,用于消息队列和交换器之间的关联,一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
exchange和queue的绑定可以是多对多的关系
2.6.connection
网络连接,比如一个tcp连接
2.7.channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的tcp连接内的虚拟连接,AMQP命令都是通过信道发出去的,不管是发布消息,订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁tcp都是非常昂贵的开销,所以引入了信道的概念,以复用一条tcp连接。
2.8.consumer
消息的消费者,,表示一个从消息队列中取得消息的客户端应用程序
2.9.virtual host
虚拟主机,表示一批交换机,消息队列和相关对象。虚拟主机是贡献相同的身份认证和加密环境的独立服务器域,每个虚拟主机本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列,交换器,绑定和权限机制,虚拟主机是AMQP概念的基础,必须在连接时指定,RabbitMQ默认的虚拟主机是/
2.10.broker
表示消息队列服务器实体
2.11.流程图
其中虚拟主机就是将broker给隔离起来,就是共用一个MQ服务,但是使用不同的虚拟主机,这样如果有多个应用:一个java,一个php,他们虽然共用一个Mq的服务器,但是虚拟主机不同,他们的操作不会互相影响。如下图:
3.安装RabbitMq
使用doker的方式进行安装
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
首先web的管理后台,我们需要开放防火墙,阿里云的esc服务器,我们需要配置安全组。
默认的账号密码是:guest
其中5672是更mq打交道的端口,收发消息的端口。
25672是集群环境的端口。
15672是访问mq的web后台的接口。
之前说的增加虚拟主机:如图
4.运行机制
AMQP中的消息路由
消息的路由过程和java开发者熟悉的jms存在一些差别,AMQP中增加了Exchange和binding的角色。
生产者把消息发布到exchange上,消息最终到达队列并被消费者接收,而binding决定交换器的消息应该发送到哪个队列。
4.1.exchange类型
exchange分发消息时根据根据类型的不同分发策略有区别,目前四种类型:
direct(默认),fanout,topic和headers。
其中direct(默认)和headers(不使用)都是点对点的
fanout,topic都是主题发布订阅的。
headers匹配AMQP消息的header而不是路由键,headers交换器和direct交换器完全一致,但性能差很多,几乎用不到。
4.1.1.direct
消息中的路由键值如果和binding中的binding key一致,交换器就将消息发到对应得队列中。
路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为"dog",则只转发路由键标记为"dog"的消息,不会转发"dog.puppy",也不会转发"dog.guary"等等,它是完全匹配,单播的模式。
这种可以理解为定向
4.1.2.fanout
每个发到fanout类型交换器的消息都会分析到所有绑定的队列上去,fanout交换器不处理路由键,知识简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上,就很像子网传播,每台子网内的主机都获得了一份复制的消息,fanout类型转发消息是最快的。
这种可以理解为广播。
4.1.3.topic
topic交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上,它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个统配符:符号"#"和符号"*",
#匹配0个或多个单词,*匹配一个单词。
这种是匹配模式。也就是通配符。
4.2.创建交换器
默认提供了7个交换机
自己创建时:
type选择需要使用的类型。
Durability的选项:Durable是持久化,另一个是非持久化,重启就没了。
其他的默认。
Arguments是设置参数,我们暂时不设置。
4.3.绑定队列
需要先创建队列
然后回到exchange交换器中,点击交换器的名称。
注意,交换器可以绑定队列,也可以绑定其他的交换器。
还要写上路由键。
注意:创建的顺序,是先创建交换器,在创建队列,在将交换器绑定队列,然后提供路由键(来路由队列的)
5.使用
首先我们在Test里面先玩一下
导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置文件:
spring.rabbitmq.host=120.77.47.129 #mq地址
spring.rabbitmq.port=5672 #端口号
spring.rabbitmq.virtual-host=/ #虚拟主机路径
启动类加注解:
@EnableRabbit
5.1.开始
首先创建交换器
找到
org.springframework.amqp.core.Exchange接口,看他的子类实现。
抽象子类 org.springframework.amqp.core.AbstractExchange
继续看子类实现
我们可以看到,他有五个实现子类,
除了四个我们我们可以上面接触过的,还有一个是CustomExchange 是一个自定义的交换器
我们暂且使用DirectExchange
5.1.1创建交换器
注入
@Autowired
RabbitAdmin rabbitAdmin;
其他重载方法:
name:交换机的名称
durable: 是否持久化 (在mq的web后台中,这个属性有两个值 1.Durable(持久),2.Transient(瞬态))
autoDelete: 是否自动删除
arguments: 参数,自己想添加什么参数就添加什么
第一个方法传入名称后,还设置了两个参数为true和false,就是持久化存储和非自动删除。
5.1.2创建队列
看其他的Queue的重载方法
name: 队列的名称
durable: 是否持久化 (在mq的web后台中,这个属性有两个值 1.Durable(持久),2.Transient(瞬态))
exclusive: 表示在被一个连接连接后,不能被其他的连接所连接,我们设置为false
autoDelete: 是否自动删除
arguments: 参数,自己想添加什么参数就添加什么
当交换器和队列都创建好,我们要让他们产生联系,那就是绑定,绑定后据可以通过路由来访问。
5.1.3.交换器绑定队列
destination: 目的地,可以是队列,也可以是其他的交换器
Binding.DestinationType destinationType: 是绑定的类型 有QUEUE,EXCHANGE两种,我们需要根据目的地类型来指定
exchange: 目前需要绑定的交换器。
routingKey:路由键
arguments:和上面的交换器和队列一样的,传入的参数,一般我们不传入。
这样我们就将交换器和队列已经绑定关系都创建好了
接下来,我们可以来发送消息
5.2.收发消息
5.2.1.发送消息
首先注入模板
@Autowired
RabbitTemplate rabbitTemplate;
这些springboot都帮我们做好了,拿来使用就可以了
补充一下,并不是只能在单元测试里面,这里只是图方便。
第一个参数是交换器的名称,第二个是路由键,第三个是发送的消息类型(这里是发送的对象)
注意:发送的是对象的话我们需要让对象实现序列化接口,否则报错,字符串或数字无所谓。
消息发送成功后:
查看rabbit的web后台
我们发现是一串序列化过后的,这是默认的jdk序列化,但是我们不需要jdk序列化,
我们要json序列化应该怎么办。
打开RabbitAutoConfiguration类,发现里面有这样的配置
就是有一个这样的消息转换器,不为空需要我们设置,但是默认是没有的。
接着看下图:
这里有一个json的转换器,我们可以使用它。
使用之前我们需要导入jackson的坐标:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.11.0</version>
</dependency>
然后创建一个转换器
此时在发送消息:
就是一个json字符串。
5.2.2.接收消息
可以携带这个几个参数,比如,Cilent是我们发送消息时的消息类型,如果不这样写,就需要自己手动转换,比较麻烦。直接写参数的化,springboot会帮我们自动转化成这个类型的消息。
注解@RabbitListener的queues是一个数组,它可以监听多个队列。
业务场景:
1.当是分布式系统时,有多个服务端都监听了该队列,也就是都有相同的代码,那么消息是怎么处理的?
答:只有一个服务端会拿到消息来处理
2.那么当有很多消息到了mq,是怎么来监听消费消息的呢?
答:会把当前的消息处理完,会把一个消息完全处理完,也就是方法运行结束,才会接收新的消息。
5.2.3.发送不同的消息怎么接收
上面我们提到了@RabbitListener注解,这个注解是可以使用到类上还有方法上的
还有一个接收消息的注解
@RabbitHandler 它是使用在方法上的。
引出问题,如果我的这个队列里面根据业务存了不同类型的消息,我们应该如何接收。
我们就可以使用这两个注解相结合的方式来接收消息。
1.首先我们发送不同类型的消息,这里就不举例了,
然后看控制台的打印效果。
我们使用@RabbitHandler来监听不同的消息类型,需要搭配@RabbitListener注解才可以。
6.消息确认
6.1.可靠抵达
.为保证消息不丢失,可靠抵达可以使用事务消息,但性能下降250倍,因此引入确认机制
.publisher confirmCallback 确认模式
.publisher returnCallback 未投递到queue退回模式
.consumer ack机制
confirmCallback:
只能表示被broker代理服务器接收到了,不能代表它已经存放到队列里了。
只有被broker代理接收到了消息就会有返回
returnCallback:
是Exchange投递到队列里面未成功才会返回信息的。
6.1.1.使用
在配置文件中再加上下面的配置,否则是失效的
spring.rabbitmq.publisher-confirms=true #开启发送端发送到borker确认 (confirmCallback)
spring.rabbitmq.publisher-returns=true #开启Exchange投递到队列的消息确认 (returnCallback)
spring.rabbitmq.template.mandatory=true #只要抵达不了队列,以异步发送优先回调到returnconfirm
写一个配置类,就是使用上面的两个结合起来来确定消息是否投递成功:
@Configuration
public class MyRabbitMQConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct //表示当MyRabbitMQConfig对象创建完成以后,执行这个方法
public void initRabbitTemplate(){
//设置确认回调,是发送端发送到borker是发送成功回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){
/**
* @param correlationData 当前消息的唯一关联属性(消息的唯一id)
* @param ack 消息是否成功收到
* @param cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("id:"+correlationData+",ack="+ack);
}
});
//投递,是Exchange投递到队列失败时回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* @param message 投递失败的消息的详细信息
* @param replyCode 回复的状态码
* @param replyText 回复的文本内容
* @param exChange 当时这个消息发送的是哪个交换机
* @param routingKey 当时这个消息用的哪个路由键
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exChange, String routingKey) {
System.out.println("correlationData="+message+",replyCode="+replyCode+",replyText="+replyText+",exChange="+exChange+",routingKey="+routingKey);
}
});
}
}
为了方便看,直接截个图:
上面代码中消息的id由于我们没有设置,是获取不到的,需要我们在发送消息时设置id,就是最后的那个参数
说完了消息投递,那么该说消费了。
6.2.消息消费端确认
像我们上面的消费者接收到的消息就会被mq移除队列,因此默认的签收的
问题:
就是如果监听的队列存入了多个数据,我们的监听的方法所在的服务器宕机了,,尽管还没有处理完数据(按理来说应该处理的才移除,但是我们发现没有处理的也被移除了),mq队列的消息已经丢失了,因为这是自动确认的
解决,手动签收,处理一个数据,签收一个。
6.2.1.使用
配置文件添加属性
spring.rabbitmq.listener.direct.acknowledge-mode=manual
总共有三个值:
manual 手动
none
auto 自动(默认不写这个配置就是这个,默认的)
这里我们就模仿一下没有签收的情况和签收的情况
开启了手动签收,就算处理消息时中断,那么消息也不会丢失,除非主动写了丢弃消息。
当然这里只是个例子而已,使用了这种方式来处理丢失消息。
未完....