1. web项目整合rabbitmq
1.1 添加相关依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- springAMQP依赖里面包含了RabbitMQ依赖
- springAMQP(Advanced Message Queuing Protocol)提供了三个功能
- 自动声明队列,交换机及其绑定关系
- 基于注解的监听器模式,异步接收消息
- 封装了RabbitTemplate工具,用于发送消息
1.2 配置文件application.yml中添加mq相关配置
spring:
rabbitmq:
host: 192.168.150.101 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: root # 用户名
password: root # 密码
至此,rabbitmq整合完成
1.3 一些消息队列的其他配置,以application.properties配置文件为例
# 每次只能获取一条消息,处理完成才能获取下一个消息
spring.rabbitmq.listener.simple.prefetch=1
2. 发布/订阅模型
- publisher:生产者发送消息,不是直接发送到队列中,而是发给交换机
- exchange:交换机。接收生产者发送的消息;转发给特定的消息队列;交换机主要有三种类型
- Fanout:广播,将消息交给所有绑定到该交换机上的队列
- Direct:定向,将消息交给符合指定routing key的队列
- Topic:通配符匹配,将消息交给所有符合routing pattern(路由模式)的队列
- queue:消息队列,接收消息,缓存消息
- consumer:消费者,订阅队列
3. Fanout
3.1 消息发送流程
- 生产者将消息将消息发送到指定交换机上(根据交换机名字)
- 交换机将消息转发给它所绑定的所有消息队列上。(广播发送)
- 订阅了消息队列的消费者就可以获得消息
3.2 示例
- 创建一个配置类,声明队列和交换机以及绑定关系
package com.example.rabbitmq1.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FanoutConfig {
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("example.fanout");
}
@Bean
public Queue fanoutQueue1() {
return new Queue("fanout.queue1");
}
@Bean
public Queue fanoutQueue2() {
return new Queue("fanout.queue2");
}
@Bean
public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
@Bean
public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
- 消息发送者
package com.example.rabbitmq1.controller;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/mq")
public class HelloController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/fanout")
public String fanout() {
// 队列名称
String exchangeName = "example.fanout";
// 消息
String message = "hello, everyone!";
// 参数分别代表:路由名称,路由键,消息
rabbitTemplate.convertAndSend(exchangeName,"",message);
return "ok";
}
}
- 消息接受者
package com.example.rabbitmq1.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) {
System.out.println("消费者1接收到Fanout消息:【" + msg + "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) {
System.out.println("消费者2接收到Fanout消息:【" + msg + "】");
}
}
4. Direct
4.1 原理
- 队列跟交换机绑定,不是任意绑定,而是指定了RoutingKey
- 消息的发送方在想交换机发送消息时,也需要指定消息的routingkey,然后交换机根据routingkey转发给特定的队列
- 一个队列可以指定多个routingkey
- 消费者还是订阅队列
4.2 实例
- 消息接受者(添加了两个消费者,同时基于注解声明了队列和交换机以及绑定关系)
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "example.direct", type = ExchangeTypes.DIRECT),
key = {"red", "blue"}
))
public void listenDirectQueue1(String msg) {
System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "example.direct", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg) {
System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
}
- 消息发送者
@RestController
@RequestMapping("/mq")
public class HelloController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/direct")
public String direct(@RequestParam("name") String name) {
// 队列名称
String exchangeName = "example.direct";
// 消息
String message = "hello, everyone!";
rabbitTemplate.convertAndSend(exchangeName, name, message);
return "ok";
}
}
5. Topic
5.1 原理
- 路由器也是根据路由键进行转发给不同的队列
- 但是Topic类型的交换机在与队列绑定routingkey时可以使用通配符
- routingkey由一个或多个单词(不是字母)组成,单词之间以"."分割
- 通配符规则
- #:匹配一个或多个单词
- *:匹配一个单词
5.2 实例
- 消息消费者
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "example.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue1(String msg){
System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "example.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenTopicQueue2(String msg){
System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");
}
- 消息发送者
@RestController
@RequestMapping("/mq")
public class HelloController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/topic")
public String topic(@RequestParam("key") String key) {
// 队列名称
String exchangeName = "example.topic";
// 消息
String message = "hello, everyone!";
rabbitTemplate.convertAndSend(exchangeName,key,message);
return "ok";
}
}
6. 配置JSON转换器
- 如果生产者向mq发送的是对象,则它默认是通过jdk序列化后再传给mq。
- 我们可以设置以JSON方式进行序列化和反序列化
6.1 导入依赖
- 生产者模块和消费者模块都需要导入依赖(用到mq一般来说都生产者和消费者是两个不同的模块,如果是一个模块那直接可以模块内通信就解决问题了)
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
6.2 向容器中添加转换器组件(配置类或者启动类中添加)
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
7. @RabbitListener与@RabbitHandler
- 一般@RabbitListener标注在类上,表明这个类处理某个消息队列上的信息
- @RabbitHandler标注在方法上,sping会根据消息反序列化后的类型寻找参数类型匹配的方法
package com.example.rabbitmq1.listener;
import com.example.rabbitmq1.entity.Person;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.stereotype.Component;
@Component
@org.springframework.amqp.rabbit.annotation.RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue3"),
exchange = @Exchange(name = "example.topic", type = ExchangeTypes.TOPIC),
key = "hello.#"
))
public class RabbitListener {
@RabbitHandler
public void listener1(String msg){
System.out.println("经过MessageConverter反序列化后消息类型是String类型的则由这个方法处理:"+msg);
}
@RabbitHandler
public void listener2(Integer msg){
System.out.println("经过MessageConverter反序列化后消息类型是Integer类型的则由这个方法处理:"+msg);
}
@RabbitHandler
public void listener3(Person person, Channel channel, Message message) {
System.out.println("经过MessageConverter反序列化后消息类型是Person类型的则由这个方法处理:"+person);
System.out.println(person);
}
}
8. 生产者模块定制RabbitTemplate实现消息发送可靠性
8.1 说明
- 对于消息生产者,可以通过定制RabbitTemplate实现一些功能
- 如给rabbitTemplate设置确认回调:当消息抵达broker就会触发此回调
- 给rabbitTemplate设置失败回调:当消息投递到queue失败会触发此回调
8.2 在生产者模块的配置类中进行以下配置
@Configuration
public class MyRabbitConfig {
//@Autowired
RabbitTemplate rabbitTemplate;
@PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
public void initRabbitTemplate() {
/**
* 1、只要消息抵达Broker就ack=true
* correlationData:当前消息的唯一关联数据(这个是消息的唯一id)
* ack:消息是否成功收到
* cause:失败的原因
*/
//设置确认回调
rabbitTemplate.setConfirmCallback((correlationData,ack,cause)->{
/**
* 1. 做好消息确认机制,手动ack
* 2. 每一个发送的消息都在数据库做好记录,定期将失败的再次发送
*/
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+"]");
});
}
}
- 主要配置了发送端消息抵达Broker的回调逻辑和发送端消息抵达queue失败的回调逻辑
8.3 在生产者模块的配置文件中配置开启消息确认
# 开启发送端消息抵达Broker确认
spring.rabbitmq.publisher-confirms=true
# 开启发送端消息抵达Queue确认
spring.rabbitmq.publisher-returns=true
# 只要消息抵达Queue,就会异步发送优先回调returnfirm
spring.rabbitmq.template.mandatory=true
- 至此,配置已经完成,当生产者模块向mq发送消息,发送成功会触发响应的回调逻辑,消息发送失败也会触发响应的回调逻辑,这样就尽可能地保证了发送消息的可靠性
9. 对于消费者模块如何尽可能地保证消息可靠性
9.1 在消费者模块配置文件中添加以下配置
# 手动ack消息,不使用默认的消费端确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
- 当消费者消费完了消息后,会手动提交一个通知,告诉mq这个消息我已经成功消费了
- 这时候mq才可以把这个消息真正地从消息队列中删去。
9.2 消费者监听器模块
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
@Autowired
private OrderService orderService;
@RabbitHandler
private void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息,准备关闭订单" + orderEntity.getOrderSn());
try {
orderService.closeOrder(orderEntity);
// 第二个参数为false则表示仅确认此条消息。如果为true则表示对收到的多条消息同时确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 第二个参数为ture表示将这个消息重新加入队列
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}
- 以上的逻辑是,当业务正常处理完,消费者会通过channel给mq手动提交一个确认。此时mq可以将消息从消息队列中删除
- 如果出现异常,消费者会通过channel会告知mq这个消息需要重新加入队列等待消费者再次消费。
9.3 补充消息队列中消息的状态
- ready:表示消息队列中还没发送给消费者的消息个数。
- unacked:表明消息队列中已经发送给消费者但是还没有收到确认的消息个数。
10 死信队列(延迟队列)
10.1 原理
- 没有消费者去订阅死信队列
- 需要给死信队列设置消息过期时间、过期后转发的交换机以及路由键
- 当信息队列中的消息过期了,会根据过期后的交换机和路由键重新发送给指定的消息队列。
- 消费者监听这个指定的消息队列就可以实现延迟队列的效果。
10.2 实现:在配置类中添加死信队列
@Configuration
public class MyRabbitMQConfig {
/**
* 死信队列
*
* @return
*/
@Bean
public Queue orderDelayQueue() {
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "order-event-exchange");
arguments.put("x-dead-letter-routing-key", "order.release.order");
arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
return queue;
}
/**
* 普通队列
*
* @return
*/
@Bean
public Queue orderReleaseQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
/**
* TopicExchange
*
* @return
*/
@Bean
public Exchange orderEventExchange() {
return new TopicExchange("order-event-exchange", true, false);
}
// 死信队列与交换机绑定
@Bean
public Binding orderCreateBinding() {
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
// 普通队列与交换机绑定
@Bean
public Binding orderReleaseBinding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
}
- 生产者会根据交换机和路由键发送给死信队列
- 死信队列中的消息达到过期时间会将消息发送给指定的普通队列
- 我们的消费者通过监听指定的普通队列即可实现延迟收到消息的功能
10.3 应用
- 可以实现定时消费消息的功能。比如三十分钟后没下单自动关闭订单
- 再配合手动提交机制,尽可能保证消息的可靠性。