RabbitMQ基础学习Full版
RabbitMQ
消息队列在软件中的应用场景
- 异步处理上(优于原先的方式)
为什么优于呢?
-
首先,通常情况下,如上图我们其实不用消息队列的情况下,其实也可以不用100ms,不用allof即可
-
那么优势在哪呢?在它消息队列的额外特点(可靠性和削峰填谷),即可保证大多数消息也就是我们发送大多数的请求能达到准确达到同时它还会控制
-
应用解耦
-
流量控制
把对应的消息过多会存到队列中,之后依次发送到对应服务
大多数应用中,可通过消息服务中间件来提升系统异步通信,扩展解耦能力(解耦的相当大的优势在于我之后某个子服务更新换代后可以不用再改原代码,而可以直接通过消息队列来更改)
消息队列的重要概念
消息代理
即是消息队列的服务器,帮你代理一下
目的地
消息发送的终点或接收的位置(有别于接收方/消费者,不是接收方啊!是消息的暂存对象)
主要两种形式的目的地(一对一&一对多)
- 队列(点对点的通信模式):每个消息只由一个消费者读取
- 主题(topic):多个消费者可以订阅同一个主题,并且每个消费者都可以接收该主题上发送的消息
springboot的使用上
- @EnableRabbit开启支持
- 自动会导入RabbitAutoConfiguration
RabbitMq的核心概念
-
message:
就是我们发送的消息(要传给消费者的东西),包括消息头(类似http的消息头,放一些约定的东西:如routing-key(路由键),priority,delivery-mode(指出该消息可能需要持久性存储)),和消息体(放具体要传送的内容) -
Publisher:
消息的生产者,向交换器(指定发送哪个)发布消息的客户端程序
-
Exchange:
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列
4类:direct(默认),fanout,topic,和headers,不同exchange转发消息策略有别 -
Queue
保存消息直到发送给消费者,消息的容器,也是消息的终点
-
Binding
绑定,用于消息队列和交换器之间的关联,一个绑定就是基于路由键将路由器和消息队列连接起来的路由规则,Exchange和Queue的绑定可以是多对多的关系
-
Connection
网络连接,如TCP
-
Channel
信道,AMQP命令都是通过信道发出去的,不管发布消息,订阅队列还是接收消息都是通过信道完成(对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以出了信道的概念,来复用一条TCP连接)
-
Consumer(从消息队列中获取信息的客户端)
-
Virtual Host:
虚拟主机(也就是一台消息队列服务器分出一个虚拟消息队列,两者互不干扰) -
Broker:(消息队列服务器实体)
核心图
Binding是交换机与队列的粘黏剂
疑问?消息队列如何知道是哪个Consumer需要消息呢?
Consumer需要去订阅topic或者queue,类似订牛奶,之后有牛奶就给你家送
exchange的类型
- headers(性能方面过于差劲,基本不用)
- direct(精准匹配:消息的routekey必须和交换机所绑定的route精准匹配才会发送(无通配符))
- fanout(无匹配机制,发送到所有订阅的消费者)
- topic(有通配符:可以通过通配符来进行匹配)
- route的组成形式是:
单词.单词.单词
- 其中.分隔的是单词,同时通配符必须整个代替单词,不存在以下情况:aaa#.444
- 通配符:(#:0个或多个字母)(*:一个字母)
- route的组成形式是:
springboot快速导入
- pom中添加
spring-boot-starter-amqp
- 查看RabbitAutoConfiguration,看一下其中的RabbitProperties了解到我们需要使用什么前缀,同时需要配置哪些信息
- 还可以观察AutoConfiguration里面为我们添加了哪些类
- 添加注解@EnableRabbit
在spring中的运用
rabbitAdmin
用于创造exchange和queue以及binding中
- exchange(学到的-->当发现通常的exchange是个接口时,可以进入然后通过ctrl+h来得到其实现类,然后使用其实现类)
@Test
void createExchange(){
//String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
Exchange exchange = new DirectExchange("dong",true,false,null);
rabbitAdmin.declareExchange(exchange);
log.info("exchange创建成功--->"+exchange.getName());
}
- queue
@Test
void createQueue(){
//String name, boolean durable, boolean exclusive, boolean autoDelete, @Nullable Map<String, Object> arguments
//exclusive,一个连接成功后,其他的无法再次连接(不符合我们正常使用,正常下是多个连接上,然后让一个获得信息)
Queue queue = new Queue("dongQueue",true,false,false,null);
rabbitAdmin.declareQueue(queue);
log.info("exchange创建成功--->"+queue.getName());
}
- Binding
@Test
void createBinding(){
//@Nullable Queue lazyQueue(惰性队列), @Nullable String destination, Binding.DestinationType destinationType, String exchange, @Nullable String routingKey, @Nullable Map<String, Object> arguments
//就是把消息存储磁盘而非内存
//读消息慢(要看IO)
Binding binding = new Binding("dongQueue", Binding.DestinationType.QUEUE,"dong","dongQueue",null);
rabbitAdmin.declareBinding(binding);
log.info("binding创建成功");
}
rabbitTemplate
封装了常用的方法例如发送信息等
@Test
void sendMessage(){
OrderItemEntity orderItemEntity = new OrderItemEntity();
orderItemEntity.setSkuName("娃哈哈h");
rabbitTemplate.convertAndSend("dongQueue22",orderItemEntity,new CorrelationData(UUID.randomUUID().toString()));
}
注意事项
- 可以直接传入任何java的对象
- 会默认使用Serializable的序列化方式
更改序列化方式为JSON
@Bean
@PostConstruct
public MessageConverter messageConverter(){
Jackson2JsonMessageConverter messageConverter = new Jackson2JsonMessageConverter();
return messageConverter;
}
在configuration中注入bean为 MessageConverter
之后会不会再去生成原先默认的Converter对象
原理如下:
在RabbitAutoConfiguration中有这么个方法,来生成rabbitTemplate的配置器
那么注意了!
像这样的ObjectProvider
于是我们正在容器中添加了对应的Json的MessageConverter
public RabbitTemplateConfigurer rabbitTemplateConfigurer(RabbitProperties properties, ObjectProvider<MessageConverter> messageConverter, ObjectProvider<RabbitRetryTemplateCustomizer> retryTemplateCustomizers)
RabbitListener以及RabbitHandler
主要负责接收消息,标志方法后,一旦监听到对应队列有消息则会自动调用
使用这两注解的前提是在配置类中添加注解@EnableRabbit
两者都是注解都可以实现接收消息的功能
区别:
- @RabbitListener
- 能标注在类和方法
- 可以标识监听哪些队列,queues =
- @RabbitHandler
- 只能标注方法
最佳实践
通常是在类上标注@RabbitListener用来指明监听的队列
然后在需要接收消息的方法上,用@RabbitHandler
优点:
之后@RabbitHandler标注的方法,会根据参数自动选择哪个方法
使用实例:
- 可以方法参数只有OrderItemEntity orderItemEntity
- 会自动帮我们拆
- channel最好还是使用一下,之后可靠传递有用
@RabbitHandler()
void getMessage(Message message, OrderItemEntity orderItemEntity, Channel channel){
//log.info("接收到消息,消息类型:{},消息头:{}",message,headers);
System.out.println(orderItemEntity.toString());
long deliveryTag = message.getMessageProperties().getDeliveryTag();
//参数1:deliveryTag(所有的消息会放到channel中,同时channel会按顺序(递增)生成一个id):
//参数2:boolean multiple 是否批处理(同一批在channel的消息都会同时确认或删除)
try {
channel.basicAck(deliveryTag,false);
} catch (IOException e) {
//long deliveryTag
// boolean multiple
// boolean requeue :是否接收完后还放回队列
try {
channel.basicNack(deliveryTag,false,false);
} catch (IOException ex) {
ex.printStackTrace();
}
//long deliveryTag, boolean requeue
//channel.basicReject(deliveryTag,false);
e.printStackTrace();
}
}
可靠投递
在各个阶段都要保证可靠
共有三个阶段
- Producer-->broker
- exchange-->queue
- queue-->consumer
解决confirmCallback
- 消息只要被 broker 接收到就会执行 confirmCallback,如果是 cluster 模式,需要所有
broker 接收到才会调用 confirmCallback。
- 被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递
到目标 queue 里
两步:
- 改配置文件
# 开启发送端确认
spring.rabbitmq.publisher-confirm-type=correlated
- 改rabbitTemplate
@PostConstruct
public void initRabbit(){
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*
* @param correlationData 当前消息的唯一关联数据(消息的唯一id)
* @param b 是否成功接收到消息
* @param s 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("correlationData"+correlationData+"----b="+b+"----s="+s);
}
});
解决returnCallback
两步:
- 改配置文件
其实只要第一行即可,第二行不是必须的,但提高性能
#开启发送端消息抵达队列的确认
spring.rabbitmq.publisher-returns=true
#只要抵达队列,以异步返送优先回调我们这个returnConfirm
spring.rabbitmq.template.mandatory=true
- 修改rabbitTemplate
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
/**
* 只要消息没有投递给指定的队列,就触发失败回调
* @param returnedMessage
*/
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
System.out.println("投递失败的消息"+returnedMessage.getMessage());
System.out.println("回复的状态码"+returnedMessage.getReplyCode());
System.out.println("回复的文本内容"+returnedMessage.getReplyText());
System.out.println("当时消息发送给哪个交换机:"+returnedMessage.getExchange());
System.out.println("当时用哪个路由键:"+returnedMessage.getRoutingKey());
}
});
}
解决ack
两步:
- 改配置
# 默认是自动签收,即只要发送到管道中即直接签收,要改成如下所示
spring.rabbitmq.listener.simple.acknowledge-mode=manual
- 在对应的接收信息的方法里修改逻辑
- 需要额外多两参数:(message,channel)
- 分别作为获取deliveryTag(代码中有解释)
- 传递该消息是否Ack或者reject or Nack
@RabbitHandler()
void getMessage(Message message, OrderItemEntity orderItemEntity, Channel channel){
//log.info("接收到消息,消息类型:{},消息头:{}",message,headers);
System.out.println(orderItemEntity.toString());
long deliveryTag = message.getMessageProperties().getDeliveryTag();
//参数1:deliveryTag(所有的消息会放到channel中,同时channel会按顺序(递增)生成一个id):
//参数2:boolean multiple 是否批处理(同一批在channel的消息都会同时确认或删除)
try {
channel.basicAck(deliveryTag,false);
} catch (IOException e) {
//long deliveryTag
// boolean multiple
// boolean requeue :是否接收完后还放回队列
try {
channel.basicNack(deliveryTag,false,false);
} catch (IOException ex) {
ex.printStackTrace();
}
//long deliveryTag, boolean requeue
//channel.basicReject(deliveryTag,false);
e.printStackTrace();
}
}