1.消息中间件使用场景
异步处理
应用解耦
流量削峰
简介
-
大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力
-
消息服务中两个重要概念:
消息代理(message broker)和目的地(destination)
当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。 -
消息队列主要有两种形式的目的地
-
队列(queue):点对点消息通信(point-to-point)
• 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移出队列
• 消息只有唯一的发送者和接受者,但并不是说只能有一个接收者 -
主题(topic):发布(publish)/订阅(subscribe)消息通信
• 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个
主题,那么就会在消息到达时同时收到消息
JMS(Java Message Service)JAVA消息服务:
基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现AMQP(Advanced Message Queuing Protocol) • 高级消息队列协议,也是一个消息代理的规范,兼容JMS RabbitMQ是AMQP的实现
Spring支持
- spring-jms提供了对JMS的支持
- spring-rabbit提供了对AMQP的支持
- 需要ConnectionFactory的实现来连接消息代理
- 提供JmsTemplate、RabbitTemplate来发送消息
- @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息
代理发布的消息 - @EnableJms、@EnableRabbit开启支持
Spring Boot自动配置
- JmsAutoConfiguration
- RabbitAutoConfiguration
- 10、市面的MQ产品
- ActiveMQ、RabbitMQ、RocketMQ、Kafka
rabbitMQ 概念
Docker安装rabbitMQ
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
4369, 25672 (Erlang发现&集群端口)
5672, 5671 (AMQP端口)
15672 (web管理后台端口)
61613, 61614 (STOMP协议端口)
1883, 8883 (MQTT协议端口)
https://www.rabbitmq.com/networking.html
测试交换机(Exchanges)和队列(Queues)
AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP 中增加了 Exchange(交换机) 和Binding 的队列。生产者把消息发布到 Exchange(交换机) 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送到那个队列。
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键,headers 交换器和 direct 交换器完全一致,都是点对点的,但性能差很多,fanout和topic都属于发布订阅模式,目前几乎用不到了,所以直接看另外三种类型:
- Direct Exchanges 直接交换机
他将消息直接交给一个指定的队列,路由键要按绑定关系完全匹配。
消息中的路由键(routing key)如果和Binding 中的 binding key 一致, 交换器就将消息发到对应的列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routingkey 标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”等等。它是完全匹配、单播的模式,点对点模式。 - Fanout Exchanges 广播类型
每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。 - Topic Exchanges 主题模式
topic 交换器通过模式匹配分配消息的 路由键属性,将路由键和某个模式进行 匹配,此时队列需要绑定到一个模式上。 它将路由键和绑定键的字符串切分成单 词,这些单词之间用点隔开。它同样也 会识别两个通配符:符号“#”和符号 “*”。#匹配0个或多个单词,*匹配一 个单词。
测试
新建交换机
新建队列
交换机绑定队列
测试点对点推送
1. Direct Exchanges 直接交换机
Nack message requeue true 收到消息后还会放回队列
Automatic ack 收到消息后销毁消息
队列
SpringBoot 整合RabbitMQ
- 引入 spring-boot-starter-amqp,高级队列协议场景启动器
给容器中自动配置了 RabbitTemplate、AmqpAdmin、CachingConnectionFactory、RabbitMessagingTemplate
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- application.yml配置
rabbitmq:
host: 127.0.0.1
port: 5672
virtual-host: /
username: guest
password: guest
- 测试RabbitMQ
- AmqpAdmin:管理组件
- RabbitTemplate:消息发送处理组件
- @RabbitListener 监听消息的方法可以有三种参数(不分数量,顺序)Object content, Message message, Channel channel
创建交换机 绑定队列 发送消息
/**
* 创建交换机
*/
@Test
public void CreateExcheang() {
//String name, boolean durable 持久化, boolean autoDelete, Map<String, Object> arguments
Exchange exchange = new DirectExchange("hello-java",true,false);
amqpAdmin.declareExchange(exchange);
log.info("创建交换机成功[{}]创建成功","hello-java");
}
/**
* 创建队列
*/
@Test
public void CreateQuery() {
//String name, boolean durable 持久化, boolean exclusive 是否排他,只能连一个, boolean autoDelete 是否自动删除
Queue queue = new Queue("hello-java",true,false,true);
amqpAdmin.declareQueue(queue);
log.info("创建队列成功[{}]创建成功","hello-java");
}
/**
* 创建绑定操作
*/
@Test
public void CreateBinding() {
//String destination 目的地,
//Binding.DestinationType destinationType 目的地类型,
// String exchange 交换机,
// String routingKey 路由键,
// Map<String, Object> arguments 自定义参数
Binding binding = new Binding("hello-java", Binding.DestinationType.QUEUE,"hello-java","hello-java",null);
amqpAdmin.declareBinding(binding);
log.info("创建绑定成功[{}]创建成功","hello-Binding");
}
/**
* 测试发送消息
*/
@Test
public void SendMessage() {
//如果发送的消息是个对象会使用序列化机制 将对象写出去,对象必须实现序列化接口
OrderReturnReasonEntity msg = new OrderReturnReasonEntity();
msg.setName("name");
msg.setId(1L);
msg.setCreateTime(new Date());
msg.setStatus(1);
//发送 json
rabbitTemplate.convertAndSend("hello-java","hello-java",msg);
log.info("消息发送完成{}","hello-Binding");
}
如果不想使用java的序列化传输数据可以使用json格式,需要配置一个消息转换器替换到默认的
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
监听指定队列接收消息
@RabbitListener 类+方法 监听哪个队列
@RabbitHandler 方法 可以重载区分不同的消息
RabbitListener必须有@EnableRabbit 注解支持(启动类添加)
RabbitMQ消息确认机制
保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制
可靠送达,即不管什么情况消息都会抵达
- publisher confirmCallback 确认模式
生产者->服务器
publisher ->Broke:
confirmCallback - publisher returnCallback 未投递到 queue 退回模式
交换机->队列
exchange->query:
returnCallback - consumer ack机制 消息确认机制
可靠抵达-ConfirmCallback-可靠抵达-ReturnCallback
-
spring.rabbitmq.publisher-confirms=true
-
在创建 connectionFactory 的时候设置 PublisherConfirms(true) 选项,开启
confirmcallback 。 -
CorrelationData:用来表示当前消息唯一性。
-
消息只要被 broker 接收到就会执行 confirmCallback,如果是 cluster 模式,需要所有
broker 接收到才会调用 confirmCallback。 -
被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递
到目标 queue 里。所以需要用到接下来的 returnCallback 。 -
spring.rabbitmq.publisher-returns=true
-
spring.rabbitmq.template.mandatory=true
-
confrim 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有
些业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到
return 退回模式。 -
这样如果未能投递到目标 queue 里将调用 returnCallback ,可以记录下详细到投递数
据,定期的巡检或者自动纠错都需要这些数据。
/**
* 定制RabbitTemplate
* 1、服务收到消息就会回调
* 1、spring.rabbitmq.publisher-confirms: true
* 2、设置确认回调
* 2、消息正确抵达队列就会进行回调
* 1、spring.rabbitmq.publisher-returns: true
* spring.rabbitmq.template.mandatory: true
* 2、设置确认回调ReturnCallback
*
* 3、消费端确认(保证每个消息都被正确消费,此时才可以broker删除这个消息)
*
*/
@PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
public void initRabbitTemplate() {
/**
* 1、只要消息抵达Broker就ack=true
* correlationData:当前消息的唯一关联数据(这个是消息的唯一id)
* ack:消息是否成功收到
* cause:失败的原因
*/
//设置确认回调
rabbitTemplate.setConfirmCallback((correlationData,ack,cause) -> {
System.out.println("confirm...correlationData["+correlationData+"]==>ack:["+ack+"]==>cause:["+cause+"]");
});
/**
* 只要消息没有投递给指定的队列,就触发这个失败回调
* message:投递失败的消息详细信息
* replyCode:回复的状态码
* replyText:回复的文本内容
* exchange:当时这个消息发给哪个交换机
* routingKey:当时这个消息用哪个路邮键
*/
rabbitTemplate.setReturnCallback((message,replyCode,replyText,exchange,routingKey) -> {
System.out.println("Fail Message["+message+"]==>replyCode["+replyCode+"]" + "==>replyText["+replyText+"]==>exchange["+exchange+"]==>routingKey["+routingKey+"]");
});
}
可靠抵达-Ack消息确认机制
- 消费者获取到消息,成功处理,可以回复Ack给Broker
– basic.ack用于肯定确认;broker将移除此消息
– basic.nack用于否定确认;可以指定broker是否丢弃此消息,可以批量
– basic.reject用于否定确认;同上,但不能批量
默认自动ack,消息被消费者收到,就会从broker的queue中移除,如果有很多消息,自动回复给服务器ack,只有一个消息ack成功,此时宕机会丢失消息,可以手动确认 - queue无消费者,消息依然会被存储,直到消费者消费
- 消费者收到消息,默认会自动ack。但是如果无法确定此消息是否被处理完成,
或者成功处理。我们可以开启手动ack模式,只要没有明确签收ack,消息就一直是unacked状态,及时消费者宕机,消息也不会丢失,会变为Reday状态,下一次有新的消费者连接就发给他
rabbitmq:
publisher-confirms: true #开启发送端确认
publisher-returns: true #消息抵达队列确认
template:
mandatory: true #只要抵达队列以异步方式优先回调returnconfirms
listener:
simple:
acknowledge-mode: manual #进入手动ack
确认消息
//确认交付参数: 1.当前消息的标签 2.是否批量确认
//在消息中获取当前消息的标签 通道内按顺序自增
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
channel.basicAck(deliveryTag,false);
} catch (IOException e) {
e.printStackTrace();
}
– 消息处理成功,ack(),接受下一个消息,此消息broker就会移除
//确认交付参数: 1.当前消息的标签 2.是否批量确认
//在消息中获取当前消息的标签 通道内按顺序自增
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
if (deliveryTag%2 == 0){
//手动签收
channel.basicAck(deliveryTag,false);
}
} catch (IOException e) {
e.printStackTrace();
}
– 消息处理失败,nack()/reject(),重新发送给其他人进行处理,或者容错处理后ack
//拒收 1.消息标签 2.批量 3.拒收后是否重新入队,不入队则丢弃
channel.basicNack(deliveryTag,false, false);
– 消息一直没有调用ack/nack方法,broker认为此消息正在被处理,不会投递给别人,此时客户
端断开,消息不会被broker移除,会投递给别人
/**
* queues 声明要监听的所有队列
* import org.springframework.amqp.core.Message
*
* 参数可以写以下类型
* 1.原生消息类型详细信息 头+体 Message message
* 2.T<发送的消息的类型> OrderReturnReasonEntity content 不用手动转化
* 3.Channel channel 当前传输数据的通道
*
* Queues 可以很多人都来监听,只要收到消息,队列就会删除消息,只能有一个人收到此消息
*
* 1.订单服务启动多个,同一个消息只能被一个服务接收
* 2.只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息
*/
@RabbitListener(queues = {"hello-java"})
public void recieveMessage(Message message, OrderReturnReasonEntity content, Channel channel){
//消息体内容
byte[] body = message.getBody();
//消息头的属性信息
MessageProperties messageProperties = message.getMessageProperties();
//确认交付参数: 1.当前消息的标签 2.是否批量确认
//在消息中获取当前消息的标签 通道内按顺序自增
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
if (deliveryTag%2 == 0){
//手动签收
channel.basicAck(deliveryTag,false);
}else {
//拒收 1.消息标签 2.批量 3.拒收后是否重新入队,不入队则丢弃
channel.basicNack(deliveryTag,false, false);
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("接收到消息:"+message);
System.out.println("内容:"+content);
}