RabbitMQ的工作模式及原理
https://baijiahao.baidu.com/s?id=1732891548341088166&wfr=spider&for=pc(部分参考)
在介绍这些概念之前,我们先看一张图,图中展示的是RabbitMQ的工作模型,根据这张图,下面理解起来就比较容易了:
其中,中间的Broker表示RabbitMQ服务,每个Broker里面至少有一个Virtual host虚拟主机,每个虚拟主机中有自己的Exchange交换机、Queue队列以及Exchange交换机与Queue队列之间的绑定关系Binding。producer(生产者)和consumer(消费者)通过与Broker建立Connection来保持连接,然后在Connection的基础上建立若干Channel信道,用来发送与接收消息。
Connection(连接)
每个producer(生产者)或者consumer(消费者)要通过RabbitMQ发送与消费消息,首先就要与RabbitMQ建立连接,这个连接就是Connection。Connection是一个TCP长连接。
Channel(信道)
Channel是在Connection的基础上建立的虚拟连接,RabbitMQ中大部分的操作都是使用Channel完成的,比如:声明Queue、声明Exchange、发布消息、消费消息等。
看到此处,你是否有这样一个疑问:既然已经有了Connection,我们完全可以使用Connection完成Channel的工作,为什么还要引入Channel这样一个虚拟连接的概念呢?因为现在的程序都是支持多线程的,如果没有Channel,那么每个线程在访问RabbitMQ时都要建立一个Connection这样的TCP连接,对于操作系统来说,建立和销毁TCP连接是非常大的开销,在系统访问流量高峰时,会严重影响系统性能。
Channel就是为了解决这种问题,通常情况下,每个线程创建单独的Channel进行通讯,每个Channel都有自己的channel id帮助Broker和客户端识别Channel,所以Channel之间是完全隔离的。
Connection与Channel之间的关系可以比作光纤电缆,如果把Connection比作一条光纤电缆,那么Channel就相当于是电缆中的一束光纤。
Virtual host(虚拟主机)
Virtual host是一个虚拟主机的概念,一个Broker中可以有多个Virtual host,每个Virtual host都有一套自己的Exchange和Queue,同一个Virtual host中的Exchange和Queue不能重名,不同的Virtual host中的Exchange和Queue名字可以一样。这样,不同的用户在访问同一个RabbitMQ Broker时,可以创建自己单独的Virtual host,然后在自己的Virtual host中创建Exchange和Queue,很好地做到了不同用户之间相互隔离的效果。
Queue(队列)
Queue是一个用来存放消息的队列,生产者发送的消息会被放到Queue中,消费者消费消息时也是从Queue中取走消息。
Exchange(交换机)
Exchange是一个比较重要的概念,它是消息到达RabbitMQ的第一站,主要负责根据不同的分发规则将消息分发到不同的Queue,供订阅了相关Queue的消费者消费到指定的消息。那Exchange有哪些分发消息的规则呢?这就要说到Exchange的4种类型了:direct、fanout、topic、headers。
在介绍这4种类型的Exchange之前,我们先来了解一下另外一个比较重要的概念:Routing key,翻译成中文就是路由键。当我们创建好Exchange和Queue之后,需要使用Routing key(通常叫作Binding key)将它们绑定起来,producer在向Exchange发送一条消息的时候,必须指定一个Routing key,然后Exchange接收到这条消息之后,会解析Routing key,然后根据Exchange和Queue的绑定规则,将消息分发到符合规则的Queue中。
接下来,我们根据上面的流程再来详细介绍下4种类型的Exchange。
RabbitMQ四种工作模式
1、direct
direct的意思是直接的,direct类型的Exchange会将消息转发到指定Routing key的Queue上,Routing key的解析规则为精确匹配。也就是只有当producer发送的消息的Routing key与某个Binding key相等时,消息才会被分发到对应的Queue上。
(上面图片只是示例,真正的实操流程看下面)
比如我们现在有一个direct类型的Exchange,它下面绑定了两个Queue,Binding key分别是es和mongodb
然后我们向该Exchange中发送一条消息,消息的Routing key是mongodb:
按照规则分析,这条消息应该被路由到pug.mq.direct.order.mongo.queue这个Queue。消息发送成功之后,我们去Queues中查看,发现确实只有pug.mq.direct.order.mongo.queue这个QUEUE接收到了一条消息。
进入这个队列,通过getMessage取出消息查看,确实是我们刚才手动发送的那条消息。
所以,direct类型的Exchange在分发消息时,必须保证producer发送消息的Routing key与Exchange和Queue绑定的Binding key相等才可以。
生产者代码示例:
1、创建direct模型配置文件
@Configuration
public class DirectOrderRabbitMQConfiguration {
/*direct模型*/
// 定义一个交换机
public static final String DIRECT_ORDER_EXCHANGE = "pug.mq.direct.order.ex";
// 定义一个路由key -- mongodb
public static final String DIRECT_ORDER_MONGODB_ROUTEING_KEY = "mongodb";
// 定义一个路由key --- es
public static final String DIRECT_ORDER_ES_ROUTEING_KEY = "es";
// 定义一个队列
public static final String DIRECT_ORDER_MONGO_QUEUE = "pug.mq.direct.order.mongo.queue";
// 定义一个队列
public static final String DIRECT_ORDER_ES_QUEUE = "pug.mq.direct.order.es.queue";
/**
* 创建交换机
* @return
*/
@Bean
public Exchange orderDirectExchange() {
return ExchangeBuilder.directExchange(DIRECT_ORDER_EXCHANGE).durable(true).build();
}
/**
* 创建mongodb队列queue
* @return
*/
@Bean
public Queue orderDirectMongoQueue() {
return QueueBuilder.durable(DIRECT_ORDER_MONGO_QUEUE).build();
}
/**
* 创建ES队列queue
* @return
*/
@Bean
public Queue orderDirectESQueue() {
return QueueBuilder.durable(DIRECT_ORDER_ES_QUEUE).build();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Binding orderDirectMongoBinding() {
return BindingBuilder
.bind(orderDirectMongoQueue()) // 绑定队列
.to(orderDirectExchange()) // 到交换机
.with(DIRECT_ORDER_MONGODB_ROUTEING_KEY) // 使用mongodb路由键
.noargs();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Binding orderDirectESBinding() {
return BindingBuilder
.bind(orderDirectESQueue()) // 绑定队列
.to(orderDirectExchange()) // 到交换机
.with(DIRECT_ORDER_ES_ROUTEING_KEY) // 使用ES路由键
.noargs();
}
}
2、发送消息时指定对应路由key
@Component
@Slf4j
public class ResponsitoryMQService {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送消息到MongoDb队列
* @param pugMessage
*/
public void publishMongoDb(PugMessage pugMessage) {
// 1: 指定交换机
rabbitTemplate.setExchange(DirectOrderRabbitMQConfiguration.DIRECT_ORDER_EXCHANGE);
// 2: 指定路由key
rabbitTemplate.setRoutingKey(DirectOrderRabbitMQConfiguration.DIRECT_ORDER_MONGODB_ROUTEING_KEY);
// 3: 发送消息指定参数
rabbitTemplate.convertAndSend(pugMessage, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 设置消息的附属信息,来达到延时或者限制的目的
MessageProperties messageProperties = message.getMessageProperties();
messageProperties.setHeader("msgtype","1");
//生产者设置消息过期时间,避免消费者处理消息出现异常导致死循环
//messageProperties.setExpiration("10000");
return message;
}
});
}
/**
* 发送消息到ES队列
* @param pugMessage
*/
public void publishESDb(PugMessage pugMessage) {
// 1: 指定交换机
rabbitTemplate.setExchange(DirectOrderRabbitMQConfiguration.DIRECT_ORDER_EXCHANGE);
// 2: 指定路由key
rabbitTemplate.setRoutingKey(DirectOrderRabbitMQConfiguration.DIRECT_ORDER_ES_ROUTEING_KEY);
// 3: 发送消息指定参数
rabbitTemplate.convertAndSend(pugMessage, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 设置消息的附属信息,来达到延时或者限制的目的
MessageProperties messageProperties = message.getMessageProperties();
messageProperties.setHeader("msgtype","1");
//messageProperties.setExpiration("10000");
return message;
}
});
}
}
3、下单选择那个队列发送消息
@GetMapping("/makeorder")
public void makeorder(Long orderId) {
// 写下单的业务
// 1保存订单
// 发送消息到mongodb
PugMessage message = new PugMessage();
message.setOpid(orderId+"");
message.setMsgtype("1");
// 选择mongodb队列发送消息
responsitoryMQService.publishMongoDb(message);
log.info("pug direct order save mongodb, id is :{}", orderId);
// 发送消息到es
// PugMessage message1 = new PugMessage();
// message1.setOpid(orderId+"");
// message1.setMsgtype("1");
// responsitoryMQService.publishESDb(message1);
// log.info("pug direct order save es, id is :{}", orderId);
}
}
消费者代码示例:
1、编写消费者对应的配置文件(和消息生产者一致,但是多了消费者的消费模式)
@Configuration
public class DirectOrderRabbitMQConfiguration {
/*direct模型*/
// 定义一个交换机
public static final String DIRECT_ORDER_EXCHANGE = "pug.mq.direct.order.ex";
// 定义一个路由key -- mongodb
public static final String DIRECT_ORDER_MONGODB_ROUTEING_KEY = "mongodb";
// 定义一个路由key --- es
public static final String DIRECT_ORDER_ES_ROUTEING_KEY = "es";
// 定义一个队列
public static final String DIRECT_ORDER_MONGO_QUEUE = "pug.mq.direct.order.mongo.queue";
// 定义一个队列
public static final String DIRECT_ORDER_ES_QUEUE = "pug.mq.direct.order.es.queue";
// 设置RabbitMQ的链接工厂实例
@Autowired
private CachingConnectionFactory connectionFactory;
/**
* 创建交换机
* @return
*/
@Bean
public Exchange orderDirectExchange() {
return ExchangeBuilder.directExchange(DIRECT_ORDER_EXCHANGE).durable(true).build();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Queue orderDirectMongoQueue() {
return QueueBuilder.durable(DIRECT_ORDER_MONGO_QUEUE).build();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Queue orderDirectESQueue() {
return QueueBuilder.durable(DIRECT_ORDER_ES_QUEUE).build();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Binding orderDirectMongoBinding() {
return BindingBuilder
.bind(orderDirectMongoQueue())
.to(orderDirectExchange())
.with(DIRECT_ORDER_MONGODB_ROUTEING_KEY).noargs();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Binding orderDirectESBinding() {
return BindingBuilder
.bind(orderDirectESQueue())
.to(orderDirectExchange())
.with(DIRECT_ORDER_ES_ROUTEING_KEY).noargs();
}
/**
* 单一消费者,确认模式是:ACK
*/
@Bean("simpleListenerContainer")
public SimpleRabbitListenerContainerFactory listenerContainer(){
// 创建消息监听器所在的容器工厂实例
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
// 为容器工厂实例设置链接工厂
factory.setConnectionFactory(connectionFactory);
// 设置消息在传输过程中的格式为JSON
factory.setMessageConverter(new Jackson2JsonMessageConverter());
// 设置消息并发实例,这里采用单一模式
factory.setConcurrentConsumers(1);
// 设置消费者并发最大数量实例
factory.setMaxConcurrentConsumers(1);
// 设置消费每个并发实例预拉取的消息数据量
factory.setPrefetchCount(1);
// 返回监听器容器工厂实例
return factory;
}
}
2、消费者消费消息
@Component
@Slf4j
public class MessageDirectConsumer {
@Autowired
private PugMessageService PugMessageService;
// 消费MongoDb队列
@RabbitListener(queues = {DirectOrderRabbitMQConfiguration.DIRECT_ORDER_MONGO_QUEUE}
,containerFactory = "simpleListenerContainer")
public void ordermonodbmessage(@Payload PugMessageDto pugMessage, Message message, Channel channel) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
String receivedExchange = message.getMessageProperties().getReceivedExchange();
String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey();
String headerMsgType = message.getMessageProperties().getHeader("msgtype");
log.info("***************pug fanout order mongodb message is :{},key:{}", pugMessage,receivedRoutingKey);
// 保存消息(对应的逻辑)
pugMessageService.saveMessage(pugMessage);
}
// 消费ES队列
@RabbitListener(queues = {DirectOrderRabbitMQConfiguration.DIRECT_ORDER_ES_QUEUE}
,containerFactory = "simpleListenerContainer")
public void orderesmessage(@Payload PugMessageDto pugMessage, Message message, Channel channel) {
String receivedExchange = message.getMessageProperties().getReceivedExchange();
String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey();
String headerMsgType = message.getMessageProperties().getHeader("msgtype");
log.info("================pug direct save es message is :{},key:{}", pugMessage,receivedRoutingKey);
// 保存消息(对应的逻辑)
pugMessageService.saveMessage(pugMessage);
}
}
2、fanout
fanout是扇形的意思,该类型通常叫作广播类型。fanout类型的Exchange不处理Routing key,而是会将发送给它的消息路由到所有与它绑定的Queue上。
(上面图片只是示例,真正的实操流程看下面)
比如我们现在有一个fanout类型的Exchange,它下面绑定了两个Queue,Binding key分别是es和mongodb:
然后我们向该Exchange中发送一条消息,消息的Routing key随便填一个值123:
按照规则分析,这条消息应该被路由到所有与该Exchange绑定的Queue,即两个Queue都应该会受到消息。消息发送成功之后,我们去Queues中查看,发现确实每个QUEUE都接收到了一条消息。
进入这三个QUEUE,通过getMessage取出消息查看,确实是我们刚才手动发送的那条消息。
所以,fanout类型的Exchange不管Routing key是什么,它都会将接收到的消息分发给所有与自己绑定了的Queue上。
生产者代码示例:
1、创建fanout模型配置文件
@Configuration
public class FanoutOrderRabbitMQConfiguration {
/*
rabbitmq遵循的:amqp协议:
- producer 生产者
- connection 连接(spring框架已经处理好了)
- channel 通道(spring框架已经处理好了)
- spring容器会利用starter机制会把rabbitmq进行服务的连接和初始化,会产生一个模板对象RabbitTemplate
- RabbitTemplate可以取完成消息的投递的工作
- exchange 交换机(如果代码中没有指定交换机:就会走到默认的交换机进行投递消息)
- routingkey 路由key(条件过滤的规则,可以为空,为空就是:Publish/Subscribe,direct,topic)
- bingding 绑定 (负责:把交换机和路由和queue进行捆绑)
- queue 队列 最终数据存储的地方(持久化问题)
- consumer 消费者
*/
/*fanout模型*/
// 定义一个交换机
public static final String FANOUT_ORDER_EXCHANGE = "pug.mq.fanout.order.ex";
// 定义一个路由key
public static final String FANOUT_ORDER_ROUTEING_KEY = "";
// 定义一个队列
public static final String FANOUT_ORDER_MONGO_QUEUE = "pug.mq.fanout.order.mongo.queue";
// 定义一个队列
public static final String FANOUT_ORDER_ES_QUEUE = "pug.mq.fanout.order.es.queue";
/**
* 创建交换机
* @return
*/
@Bean
public Exchange orderFanoutExchange() {
return ExchangeBuilder.fanoutExchange(FANOUT_ORDER_EXCHANGE).durable(true).build();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Queue orderFanoutMongoQueue() {
return QueueBuilder.durable(FANOUT_ORDER_MONGO_QUEUE).build();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Queue orderFanoutESQueue() {
return QueueBuilder.durable(FANOUT_ORDER_ES_QUEUE).build();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Binding orderFanoutMongoBinding() {
return BindingBuilder
.bind(orderFanoutMongoQueue())
.to(orderFanoutExchange())
.with(FANOUT_ORDER_ROUTEING_KEY).noargs();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Binding orderFanoutESBinding() {
return BindingBuilder
.bind(orderFanoutESQueue())
.to(orderFanoutExchange())
.with(FANOUT_ORDER_ROUTEING_KEY).noargs();
}
}
消费者代码示例:
1、编写消费者对应的配置文件(和消息生产者一致,但是多了消费者的消费模式)
@Configuration
public class FanoutOrderRabbitMQConfiguration {
/*
rabbitmq遵循的:amqp协议:
- producer 生产者
- connection 连接(spring框架已经处理好了)
- channel 通道(spring框架已经处理好了)
- spring容器会利用starter机制会把rabbitmq进行服务的连接和初始化,会产生一个模板对象RabbitTemplate
- RabbitTemplate可以取完成消息的投递的工作
- exchange 交换机(如果代码中没有指定交换机:就会走到默认的交换机进行投递消息)
- routingkey 路由key(条件过滤的规则,可以为空,为空就是:Publish/Subscribe,direct,topic)
- bingding 绑定 (负责:把交换机和路由和queue进行捆绑)
- queue 队列 最终数据存储的地方(持久化问题)
- consumer 消费者
*/
/*fanout模型*/
// 定义一个交换机
public static final String FANOUT_ORDER_EXCHANGE = "pug.mq.fanout.order.ex";
// 定义一个路由key
public static final String FANOUT_ORDER_ROUTEING_KEY = "";
// 定义一个队列
public static final String FANOUT_ORDER_MONGO_QUEUE = "pug.mq.fanout.order.mongo.queue";
// 定义一个队列
public static final String FANOUT_ORDER_ES_QUEUE = "pug.mq.fanout.order.es.queue";
// 设置RabbitMQ的链接工厂实例
@Autowired
private CachingConnectionFactory connectionFactory;
/**
* 创建交换机
* @return
*/
@Bean
public Exchange orderFanoutExchange() {
return ExchangeBuilder.fanoutExchange(FANOUT_ORDER_EXCHANGE).durable(true).build();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Queue orderFanoutMongoQueue() {
return QueueBuilder.durable(FANOUT_ORDER_MONGO_QUEUE).build();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Queue orderFanoutESQueue() {
return QueueBuilder.durable(FANOUT_ORDER_ES_QUEUE).build();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Binding orderFanoutMongoBinding() {
return BindingBuilder
.bind(orderFanoutMongoQueue())
.to(orderFanoutExchange())
.with(FANOUT_ORDER_ROUTEING_KEY).noargs();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Binding orderFanoutESBinding() {
return BindingBuilder
.bind(orderFanoutESQueue())
.to(orderFanoutExchange())
.with(FANOUT_ORDER_ROUTEING_KEY).noargs();
}
/**
* 单一消费者,确认模式是:ACK
*/
@Bean("simpleListenerContainer")
public SimpleRabbitListenerContainerFactory listenerContainer(){
// 创建消息监听器所在的容器工厂实例
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
// 为容器工厂实例设置链接工厂
factory.setConnectionFactory(connectionFactory);
// 设置消息在传输过程中的格式为JSON
factory.setMessageConverter(new Jackson2JsonMessageConverter());
// 设置消息并发实例,这里采用单一模式
factory.setConcurrentConsumers(1);
// 设置消费者并发最大数量实例
factory.setMaxConcurrentConsumers(1);
// 设置消费每个并发实例预拉取的消息数据量
factory.setPrefetchCount(1);
// 返回监听器容器工厂实例
return factory;
}
}
2、消费者消费消息
@Component
@Slf4j
public class MessageDirectConsumer {
@Autowired
private PugMessageService PugMessageService;
// 消费MongoDb队列
@RabbitListener(queues = {FanoutOrderRabbitMQConfiguration.FANOUT_ORDER_MONGO_QUEUE}
,containerFactory = "simpleListenerContainer")
public void ordermonodbmessage(@Payload PugMessageDto pugMessage, Message message, Channel channel) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
String receivedExchange = message.getMessageProperties().getReceivedExchange();
String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey();
String headerMsgType = message.getMessageProperties().getHeader("msgtype");
log.info("***************pug fanout order mongodb message is :{},key:{}", pugMessage,receivedRoutingKey);
// 保存消息(对应的逻辑)
pugMessageService.saveMessage(pugMessage);
}
// 消费ES队列
@RabbitListener(queues = {FanoutOrderRabbitMQConfiguration.FANOUT_ORDER_ES_QUEUE}
,containerFactory = "simpleListenerContainer")
public void orderesmessage(@Payload PugMessageDto pugMessage, Message message, Channel channel) {
String receivedExchange = message.getMessageProperties().getReceivedExchange();
String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey();
String headerMsgType = message.getMessageProperties().getHeader("msgtype");
log.info("================pug direct save es message is :{},key:{}", pugMessage,receivedRoutingKey);
// 保存消息(对应的逻辑)
pugMessageService.saveMessage(pugMessage);
}
}
3、topic
topic的意思是主题,topic类型的Exchange会根据通配符对Routing key进行匹配,只要Routing key满足某个通配符的条件,就会被路由到对应的Queue上。通配符的匹配规则如下:
● Routing key必须是一串字符串,每个单词用“.”分隔;
● 符号“#”表示匹配一个或多个单词;
● 符号“*”表示匹配一个单词。
例如:“*.123” 能够匹配到 “abc.123”,但匹配不到 “abc.def.123”;“#.123” 既能够匹配到 “abc.123”,也能匹配到 “abc.def.123”。
比如我们现在有一个topic类型的Exchange,它下面绑定了2个Queue,Binding key分别是 *.mongodb.# 和 #.es
然后我们向该Exchange中发送一条消息,消息的Routing key为:user.mongodb。
按照规则分析,user.mongodb这个Routing key只可以匹配到 “*.mongodb.#” ,所以,这条消息应该被路由到pug.mq.topic.order.mongo.queue这个Queue中。消息发送成功之后,我们去Queues中查看,发现结果符合我们的预期。
进入这个QUEUE,通过getMessage取出消息查看,确实是我们刚才手动发送的那条消息。
生产者代码示例:
1、创建topic模型配置文件
@Configuration
public class TopicOrderRabbitMQConfiguration {
/*topic模型*/
// 定义一个交换机
public static final String TOPIC_ORDER_EXCHANGE = "pug.mq.topic.order.ex";
// 定义一个路由key -- mongodb #0个或者多个 *至少要有一个
// routingkey = #.mongodb.es.# --- a.b.c.mongodb.es / mongodb.es /mongodb.es.a.b/
// routingkey = *.mongodb.es.*
// ------------ a.mongodb.es.b
public static final String TOPIC_ORDER_MONGODB_ROUTEING_KEY = "*.mongodb.#";
// 定义一个路由key --- es
public static final String TOPIC_ORDER_ES_ROUTEING_KEY = "#.es";
// 定义一个队列
public static final String TOPIC_ORDER_MONGO_QUEUE = "pug.mq.topic.order.mongo.queue";
// 定义一个队列
public static final String TOPIC_ORDER_ES_QUEUE = "pug.mq.topic.order.es.queue";
// 设置RabbitMQ的链接工厂实例
@Autowired
private CachingConnectionFactory connectionFactory;
/**
* 创建交换机
* @return
*/
@Bean
public Exchange orderTopicExchange() {
return ExchangeBuilder.topicExchange(TOPIC_ORDER_EXCHANGE).durable(true).build();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Queue orderTopicMongoQueue() {
return QueueBuilder.durable(TOPIC_ORDER_MONGO_QUEUE).build();
}
/**
* 创建队列queue
* @return
*/
@Bean
public Queue orderTopicESQueue() {
return QueueBuilder.durable(TOPIC_ORDER_ES_QUEUE).build();
}
/**
* 绑定队列到到交换机
* @return
*/
@Bean
public Binding orderTopicMongoBinding() {
return BindingBuilder
.bind(orderTopicMongoQueue())
.to(orderTopicExchange())
.with(TOPIC_ORDER_MONGODB_ROUTEING_KEY).noargs();
}
/**
* 绑定队列到到交换机
* @return
*/
@Bean
public Binding orderTopicESBinding() {
return BindingBuilder
.bind(orderTopicESQueue())
.to(orderTopicExchange())
.with(TOPIC_ORDER_ES_ROUTEING_KEY).noargs();
}
}
2、生产者发送消息的实现类
@Component
@Slf4j
public class ResponsitoryTopicMQService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void publishMongoDb(PugMessage pugMessage, String routeKey) {
rabbitTemplate.setMandatory(true);
// 设置消息发送确认机制,即发送成功时输出日志
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
// 这里如果能收到回执,说明rabbitmq一定收到了消息。
log.info("消息发送成功!!!!,correlationData:{},ack:{},casuse:{}", correlationData, ack, cause);
});
// 设置消息发送确认机制,即发送完消息后输出反馈信息,如消息是否丢失等。
rabbitTemplate.setReturnsCallback((resultMessage) -> {
log.info("消息确认了:,message:{},replycode:{},replytext:{},exchange:{},routingKey:{}",
resultMessage.getMessage().getBody(), resultMessage.getReplyCode(), resultMessage.getReplyText(), resultMessage.getExchange(), resultMessage.getRoutingKey());
});
// 1: 指定交换机
rabbitTemplate.setExchange(TopicOrderRabbitMQConfiguration.TOPIC_ORDER_EXCHANGE);
// 2: 指定路由key
rabbitTemplate.setRoutingKey(routeKey);
// 3: 发送消息指定参数
rabbitTemplate.convertAndSend(pugMessage, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 设置消息的附属信息,来达到延时或者限制的目的
MessageProperties messageProperties = message.getMessageProperties();
messageProperties.setHeader("msgtype", "1");
//messageProperties.setExpiration("10000");
return message;
}
});
}
public void publishESDb(PugMessage pugMessage, String routeKey) {
rabbitTemplate.setMandatory(true);
// 设置消息发送确认机制,即发送成功时输出日志 只要你的消息投递到了交换机,就代表以及收到了消息,rabbitserver就会给你一个确认操作.
// 哪怕投递到了一个不存在的路由key或者交换机中都会进入setConfirmCallback
// 但是在开发的时候,我们最好还是要判断一个下ack是否收到回执,如果收到rabbItmqserver服务器给与回执ack=true,
// 代表生产者已经把消息已经到了交换机中
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if(!ack){
// 这里什么时候进入:就是交换机没有收到生产者提供的消息就进入到setConfirmCallback,ack就是false
System.out.println("没有收到rabbitmq server 确认.....");
return;
}
log.info("1--------------------消息发送成功!!!!,correlationData:{},ack:{},casuse:{}", correlationData, ack, cause);
});
// 设置消息发送确认机制,即发送完消息后输出反馈信息,如消息是否丢失等。
// ReturnCallback消息没有正确到达队列时触发回调,如果正确到达队列不执行
//
rabbitTemplate.setReturnsCallback((resultMessage) -> {
System.out.println("2=================returnCallback触发。消息路由到queue失败===========");
System.out.println("msg=" + new String(resultMessage.getMessage().getBody()));
System.out.println("replyCode=" + resultMessage.getReplyCode());
System.out.println("replyText=" + resultMessage.getReplyText());
System.out.println("exchange=" + resultMessage.getExchange());
});
// 1: 指定交换机
rabbitTemplate.setExchange(TopicOrderRabbitMQConfiguration.TOPIC_ORDER_EXCHANGE);
// 2: 指定路由key
rabbitTemplate.setRoutingKey(routeKey);
// 3: 发送消息指定参数
rabbitTemplate.convertAndSend(pugMessage, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 设置消息的附属信息,来达到延时或者限制的目的
MessageProperties messageProperties = message.getMessageProperties();
// 附属消息
messageProperties.setHeader("msgtype", "1");
// 附属消息中,添加默认重试次数为0
messageProperties.setHeader("retry-count", "0");
//messageProperties.setExpiration("10000");
return message;
}
});
}
}
3、生产者下单
@Service
@Slf4j
public class OrderTopicServiceProducer {
@Autowired
private ResponsitoryTopicMQService responsitoryTopicMQService;
/**
* 模拟下单
*
* @param orderId
*/
public void makeorder(Long orderId) {
// 写下单的业务
// 1保存订单
// 发送消息到mongodb es
PugMessage message = new PugMessage();
message.setOpid(orderId+"");
message.setMsgtype("1");
// order.mongodb.es // mongodb es
// order.mongodb.a //mongodb
// order.es//es
responsitoryTopicMQService.publishESDb(pugMessage,"user.mongodb"); // 第二个参数是路由键
log.info("pug topic order save mongodb, id is :{}", orderId);
}
}
消费者代码示例:
1、创建topic模型配置文件
@Component
@Slf4j
public class MessageTopicConsumer {
@RabbitListener(queues = {TopicOrderRabbitMQConfiguration.TOPIC_ORDER_MONGO_QUEUE},
containerFactory = "simpleTopicListenerContainerMANUALAck")
public void ordermonodbmessage(@Payload PugMessageDto messageDto, Message message, Channel channel) throws IOException, InterruptedException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
// 故意报错
System.out.println(1 / 0);
channel.basicAck(deliveryTag, false);
log.info("================pug topic order mongodb message is :{}", messageDto);
} catch (Exception ex) {
channel.basicAck(deliveryTag, false);
//当消息回滚到消息队列时,这条消息不会回到队列尾部,而是仍是在队列头部。
//这时消费者会立马又接收到这条消息进行处理,接着抛出异常,进行 回滚,如此反复进行
//而比较理想的方式是,出现异常时,消息到达消息队列尾部,这样既保证消息不回丢失,又保证了正常业务的进行。
//因此我们采取的解决方案是,将消息进行应答。
//这时消息队列会删除该消息,同时我们再次发送该消息 到消息队列,这时就实现了错误消息进行消息队列尾部的方案
//1.应答
//2.重新发送到MQ中
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
.contentType("application/json").build();
// 重新把消息发动自己的交换机和队列中继续消费
channel.basicPublish(message.getMessageProperties().getReceivedExchange(),
message.getMessageProperties().getReceivedRoutingKey(), basicProperties,
message.getBody());
}
}
@RabbitListener(queues = {TopicOrderRabbitMQConfiguration.TOPIC_ORDER_ES_QUEUE},
containerFactory = "simpleTopicListenerContainerAutoACK")
public void orderesmessage(@Payload PugMessageDto messageDto, Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
// basicAck自动应答
//System.out.println(1 / 0);
// 处理消息的逻辑
// 参数1:消息的id
// 参数2:消息是否进行批量确认,如果你默认配置,就写false,代表就把当前的消息进行确认.
channel.basicAck(deliveryTag, false);
log.info("================pug topic order es message is :{}", messageDto);
} catch (Exception ex) {
// 发送消息时将retry-count的值放进Header中
Map<String, Object> headers = message.getMessageProperties().getHeaders();
//重试次数
Integer retryCount;
String mapKey = "retry-count";
if (!headers.containsKey(mapKey)) {
retryCount = 0;
} else {
retryCount = (Integer) headers.get(mapKey);
}
if (retryCount++ < 3) {
log.info("已经重试 " + retryCount + " 次");
headers.put("retry-count", retryCount);
//当消息回滚到消息队列时,这条消息不会回到队列尾部,而是仍是在队列头部。
//这时消费者会立马又接收到这条消息进行处理,接着抛出异常,进行 回滚,如此反复进行
//而比较理想的方式是,出现异常时,消息到达消息队列尾部,这样既保证消息不回丢失,又保证了正常业务的进行。
//因此我们采取的解决方案是,将消息进行应答。
//这时消息队列会删除该消息,同时我们再次发送该消息 到消息队列,这时就实现了错误消息进行消息队列尾部的方案
//1.应答
channel.basicAck(deliveryTag, false);
//2.重新发送到MQ中
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
.contentType("application/json").headers(headers).build();
// 重新把消息发动自己的交换机和队列中继续消费
channel.basicPublish(message.getMessageProperties().getReceivedExchange(),
message.getMessageProperties().getReceivedRoutingKey(), basicProperties,
message.getBody());
} else {
log.info("现在重试次数为:" + retryCount);
/**
* 重要的操作 存盘
* 手动ack
* channel.basicAck(deliveryTag,false);
* 通知人工处理
* log.error("重试三次异常,快来人工处理");
*/
//消息存盘
// MsgLog msgLog = new MsgLog();
// msgLog.setMsgId(msg.getMsgId());
// msgLog.setMsg(new String(message.getBody(),"utf-8"));
// msgLog.setExchange(message.getMessageProperties().getReceivedExchange());
// msgLog.setRoutingKey(message.getMessageProperties().getReceivedRoutingKey());
// msgLog.setTryCount(retryCount);
// msgLog.setStatus(MsgLogStatusEnum.FAIL.getStatus());
// msgLogService.save(msgLog);
/**
* 不重要的操作放入 死信队列
* 消息异常处理:消费出现异常后,延时几秒,然后从新入队列消费,直到达到ttl超时时间,再转到死信,证明这个信息有问题需要人工干预
*/
//休眠2s 延迟写入队列,触发转入死信队列(需要添加死信队列,然后将正常队列绑定到死信交换机上)
//Thread.sleep(2000);
//channel.basicNack(deliveryTag, false, true);
}
}
}
}
辅助理解三种确认方式的区别
channel.basicReject(deliveryTag, true);
basic.reject方法拒绝deliveryTag对应的消息,第二个参数是否requeue,true则重新入队列,否则丢弃或者进入死信队列。
该方法reject后,该消费者还是会消费到该条被reject的消息。
channel.basicNack(deliveryTag, false, true);
basic.nack方法为不确认deliveryTag对应的消息,第二个参数是否应用于多消息,第三个参数是否requeue,与basic.reject区别就是同时支持多个消息,可以nack该消费者先前接收未ack的所有消息。nack后的消息也会被自己消费到。
channel.basicRecover(true);
basic.recover是否恢复消息到队列,参数是是否requeue,true则重新入队列,并且尽可能的将之前recover的消息投递给其他消费者消费,而不是自己再次消费。false则消息会重新被投递给自己。