分析MQ的好处
1、异步处理
2、模块解耦
3、流量控制
解耦问题,模块代码有可能随时改动,比如订单系统,给库存系统数据,但是库存系统总是修改代码,订单系统也要跟着修改,如果了消息队列,订单系统的数据直接存储到消息队列中,库存系统直接从消息队列拿想要的数据就好,有了消息队列直接数据存储到消息队列,不用关系模块代码改动
概述
消息代理:就是MQ服务器,专门接受消息和发送消息。
目的地:分为两种,一种是队列,一种是主题。分别对应点对点模式和发布订阅模式
下面的是对目的地的详细介绍
JMS:是java消息服务,是Java消息代理的规范
AMQP:是一种消息队列的协议
JMS和AMQP实现消息队列对比
Spring支持MQ的整合
理解@JmsListener,@RabbitListener,JmsAutoConfiguration,RabbitAutoConfiguration,@EnableJms,@EnableRabbit的作用和使用,以及对应的template的使用
理解概念
Message,publisher,Exchange,Consumer,virtual host,Broker都是什么,Broker就是代表服务器。
概念图
交换机:负责接收消息。
队列:负责存储消息。
他们之间存在绑定关系。
执行流程:
生产者发送消息,会和broker消息代理建立一个长连接,这个长连接里面包含多个channal通道,消息通过这些通道传递消息
消息里面包含头和体,头带着route-key(路由键),消息先到了broker消息代理,也就是MQ服务器,然后到交换机。交换机根据消息中的头的路由建,去找对应的队列,并将消息存储到队列中。
消费者,也会这broker消息代理,建立一个长连接,内部也是包含很多通道,通过通道来进行接收消息
补充:虚拟主机,可以让一个MQ当做多个MQ进行使用。虚拟主机之间是相互隔离,数据也会到对应的虚拟主机中。
安装
简单介绍web登录界面
overview介绍
Overview中的totals选项
Overview中的ports and contexts,查看对应的协议使用什么端口
如何实现将以前的MQ的老配置迁移到刚刚安装的MQ中
在旧的MQ中的overview中的export difinitions,点击下载红色箭头,配置信息下载下来
在新的MQ中的overview的import difinitions,进行导入
Connection介绍
Channels介绍
Exchanges介绍
如何添加一个新的交换机,在exchanges中的add a new exchange按钮进行添加
Queues介绍
如何添加队列
Admin介绍
用户管理
虚拟主机介绍
查看自己创建的虚拟主机
进入后可修改权限
删除本虚拟主机,进入该虚拟主机内部。
设置最大连接数
显示集群信息
Exchange类型
direct,headers是点对点,fanout,topic是发布订阅,fanout是扇出。
dierect交换机
实现条件:要求路由建完全匹配。
fanout交换机和topic交换机
fanout交换机实现条件:他是广播模式,也就是不需要路由建,直接已一个点扩散出去。类似散弹枪。
topic交换机实现条件:他和fanout类似,但是更加灵活,需要路由Key,但是他的键匹配有点类似正则表达式匹配。注意两个关键的通配符,“*”表示匹配一个单词,“#”表示匹配0个或者多个,他们是以点分割。
实战操作
创建四个队列
下面的是对应的四个队列
创建交换机,并点击进入创建的交换机
进入创建的交换机内部,进行绑定队列
下面是交换机和队列的绑定关系
下面的是topic的绑定形式
点击交换机名称发送消息
发送之后看队列Queues选项卡,确实有消息
点击上图对应的队列进入下面。设置队列处理消息的方式
点击获取消息
在此查看队列发现队列没有消息了
SpringBoot整合MQ
整合:
maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置文件
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
添加@EnableRabbit注解
编写测试类
通过AmqpAdmin,创建交换机,队列,和绑定关系。RabbitTemplate是用来发送消息的
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",reasonEntity);
@Slf4j
@SpringBootTest
class GulimallOrderApplicationTests {
@Autowired
AmqpAdmin amqpAdmin;
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 创建交换机
*/
@Test
void createExchange() {
/**
* 交换机名字 是否持久化 不自动删除 交换机创建的时候还可以创建一些参数
* public DirectExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments) {
*
*/
DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
amqpAdmin.declareExchange(directExchange);
log.info("Exchange[{}]创建成功","hello-java-exchange");
}
/**
* 创建队列
*/
@Test
public void createQueue(){
/**
* 队列名字 是否持久化 只有一个连接的话,其他就不能连接 是否自动删除 创建队列的时候还可以创建一些参数
* public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, @Nullable Map<String, Object> arguments) {
*/
Queue queue = new Queue("hello-java-queue",true,false,false);
amqpAdmin.declareQueue(queue);
log.info("Queue[{}]创建成功","hello-java-queue");
}
/**
* 队列和交换机进行绑定
*/
@Test
public void createBinding(){
/**
* 目的地 目的地类型 交换机 路由键 自定义参数
* public Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, @Nullable Map<String, Object> arguments) {
* 将exchange指定的交换机和destination目的地进行绑定,然后使用routingKey作为指定的路由键
*/
Binding binding = new Binding("hello-java-queue", Binding.DestinationType.QUEUE,"hello-java-exchange","hello.java",null);
amqpAdmin.declareBinding(binding);
log.info("Binding[{}]创建成功","hello-java-binding");
}
/**
* 测试给rabbitmq进行发消息
*/
@Test
public void sendMessageTest(){
/**
* 第一个参数:交换机
* 第二个参数:路由建
* 第三个参数:发送的消息是什么
*/
String msg = "Hello World!";
for (int i = 0; i <10 ; i++) {
OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
reasonEntity.setId(1L);
reasonEntity.setCreateTime(new Date());
reasonEntity.setName("哈哈-"+i);
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",reasonEntity);
log.info("消息发送完成{}",reasonEntity);
}
}
}
编写MQ的配置类,消息转换器。可以将其转换成Json ,通过MessageConverter,将其序列化的进行转换JSon数据
/**
* 给容器中放一个消息转换器
*/
@Configuration
public class MyRabbitConfig {
// @Autowired
RabbitTemplate rabbitTemplate;
@Primary
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
this.rabbitTemplate = rabbitTemplate;
rabbitTemplate.setMessageConverter(messageConverter());
initRabbitTemplate();
return rabbitTemplate;
}
/**
* 使用JSON序列化机制,进行消息的转换
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/**
* 定制RabbitTemplate
* 1、服务器收到消息就回调
* 1、spring.rabbitmq.publisher-confirms=true
* 2、设置确认回调ConfirmCallback
* 2、消息正确抵达队列进行回调
* 1、 spring.rabbitmq.publisher-returns=true
* spring.rabbitmq.template.mandatory=true
* 2、设置确认回调ReturnCallback
*
* 3、消费端确认(保证每个消息被正确消费,此时才可以broker删除这个消息)。
* spring.rabbitmq.listener.simple.acknowledge-mode=manual 手动签收
* 1、默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
* 问题:
* 我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了。就会发生消息丢失;
* 消费者手动确认模式。只要我们没有明确告诉MQ,货物被签收。没有Ack,
* 消息就一直是unacked状态。即使Consumer宕机。消息不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他
* 2如何去签收:
* channel.basicAck(deliveryTag,false);签收;业务成功完成就应该签收
* channel.basicNack(deliveryTag,false,true);拒签;业务失败就拒签收
*/
// @PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
public void initRabbitTemplate(){
//设置一个确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*只要消息抵达MQ服务器,那么ACK聚等于true
* @param correlationData 当前消息的唯一关联数据,每一个消息发送的时候可以给一个唯一id
* @param ack 消息是否成功收到
* @param cause 失败的原因
*
*只要MQ代理收到消息,那么confirm就会自动回调
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
//服务器收到消息
System.out.println("confirm...correlationData["+correlationData+"]==>ack["+ack+"]==>cause["+cause+"]");
}
});
//设置抵达队列的确认回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* 只要消息没有投递给指定的队列,就触发这个returnedMessage失败回调方法
* @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("Fail Message["+message+"]==>replyCode["+replyCode+"]==>replyText["+replyText+"]==>exchange["+exchange+"]==>routingKey["+routingKey+"]");
}
});
}
}
监听消息
上面message输出的内容是什么。如下
//获取消息体
byte[] body = message.getBody();就能获取到消息体的内容。
//消息头属性信息
MessageProperties properties = message.getMessageProperties();
三个参数的介绍
1、Message message:原生消息详细信息。头+体
* 2、T<发送的消息的类型> OrderReturnReasonEntity content;
* 3、Channel channel:当前传输数据的通道
@RabbitHandler
public void recieveMessage(Message message,
OrderReturnReasonEntity content,
Channel channel)
不同场景下,消息如何处理
Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息
* 场景:
* 1)、订单服务启动多个;同一个消息,只能有一个客户端收到
* 2)、 当业务处理时间很长,只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息
@RabbitListener和@RabbitHandler使用
他们之间如何使用
监听消息
注意@RabbitListener放在这个ava的类上
发送消息
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",reasonEntity);
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",entity);
下面的是可以带上消息的唯一表示。
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",reasonEntity,new CorrelationData(UUID.randomUUID().toString());
这里有人会说如果随便指定一个uuid,怎么知道服务端收到的是哪一个消息呢
解决:发送消息之后,可以将消息保存到数据库中,如果服务端收到了这个消息,就在数据库中说已经收到消息了(修改状态),如果消息没有收到,就从数据库中遍历,看那些消息是没收到状态(这里会使用本地事物表的方式实现可靠抵达),如果从数据库中遍历的数据没有送达,做一个定时任务重新在发送一遍。
消息确认机制
问题:什么是可靠抵达,为什么要可靠抵达?
在分布式系统中,会出现,网络抖动,消费者宕机,MQ服务宕机,生产者服务宕机等一系列问题,都可能导致消息的丢失。以前可以使用事物消息(发送和接收一连串动作都完成并响应,这就是事物消息),但是性能下降250倍
为了保证可靠抵达,就要保证publisher和Consumer,一个能发送出去,一个能接收到,这里就用到消息确认机制
消息确认机制关键点
1、生产者(发送端),存在两次回调,一次是发送到broker(也就是MQ服务)用到了confirmCallback确认模式,一次是交换机发送到队列,用到returnCallback退回模式。
2、消费方(接收方),消费者从队列中接收到消息后,给与一ack确认机制,告诉他我接收到消息了
生产者分析
第一阶段可靠抵达MQ服务器
confirmCallback触发时机,只要消息一到MQ服务器就触发
实现步骤一:
代码中只需要配置spring.rabbitmq.publisher-confirms=true,这个注解的作用就是开启发送端确认或者在编码中在连接工厂中设置
实现步骤二:
在配置类中编写覆盖rabbitteplate的ConfirmCallback方法
确认回调ConfirmCallback是在rabbittemplate里面的对应的有三个参数
* @param correlationData 当前消息的唯一关联数据,每一个消息发送的时候可以给一个唯一id(就是区别消息的标识)
* @param ack 消息是否成功收到
* @param cause 失败的原因
注意:只要消息抵达MQ服务器,那么ACK聚等于true
打印输出
第二阶段,消息由交换机到队列
退回回调触发时机,只要消息没有发送成功就会触发该回调,
实现步骤一:
实现步骤二:
思想和上面确认回调差不多
下面对应的参数介绍
/**
* 只要消息没有投递给指定的队列,就触发这个returnedMessage失败回调方法
* @param message 哪个消息投递失败了,失败消息的具体内容
* @param replyCode 回复的状态码
* @param replyText 回复的文本内容
* @param exchange 当前这个消息发给哪个交换机
* @param routingKey 当前这个消息发送的时候,指定的哪个路由键
*/
如何测试触发这个方法,简单修改发送的路由key
//设置抵达队列的确认回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* 只要消息没有投递给指定的队列,就触发这个returnedMessage失败回调方法
* @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("Fail Message["+message+"]==>replyCode["+replyCode+"]==>replyText["+replyText+"]==>exchange["+exchange+"]==>routingKey["+routingKey+"]");
}
});
下面对应的输出的结果
消费者分析
消费端默认情况下,自动会ack确认,这种模式会出现问题,如果当消费端执行过程中宕机,自动ack会将那些没有处理的消息,自动消费掉,这是一个很严重的严重的问题。
解决方式:手动确认
* 3、消费端确认(保证每个消息被正确消费,此时才可以broker删除这个消息)。
* spring.rabbitmq.listener.simple.acknowledge-mode=manual 手动签收
* 1、默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
* 问题:
* 我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了。就会发生消息丢失;
* 消费者手动确认模式。只要我们没有明确告诉MQ,货物被签收。没有Ack,
* 消息就一直是unacked状态。即使Consumer宕机。消息不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他
* 2如何去签收:
* channel.basicAck(deliveryTag,false);签收;业务成功完成就应该签收
* channel.basicNack(deliveryTag,false,true);拒签;业务失败就拒签收
AcK确认机制
消费端宕机,就会将unacked转化为ready状态
实现步骤一:
手动签收
spring.rabbitmq.listener.simple.acknowledge-mode=manual
实现步骤二:
在监听的地方编写ack确认
basicNack和basicReject和basicAck联系
basicAck类似收货
第一个参数是id,第二个参数是否非批量签收。现在已经设定为只签收当前这个货物,也即做一个消息给它确认一个
channel.basicAck(deliveryTag,false);
basicNack类似退货
三个参数:long deliveryTag, boolean multiple, boolean requeue
第一个参数是Id,第二个参数是否可以批量操作,第三个参数requeue=false 丢弃 requeue=true 发回服务器,服务器重新入队。
channel.basicNack(deliveryTag,false,true);
basicReject类似退货
和上面的basicNack差不多,但是少了一个批量操作
channel.basicReject(deliveryTag,true);