rabbitmqDemo,消息队列的使用
Spring Boot版本:2.3.4.RELEASE
目录
- 使用RabbitMq的准备工作
- 消息队列的三个常用交换机
- 商品订单超时提醒的实现
- 消费过程出现异常如何处理
使用RabbitMq的准备工作
rabbitmq环境准备(Docker容器):
docker run --name myrabbit -p 15672:15672 -p 5672:5672 -p 25672:25672 -p 61613:61613 -p 1883:1883 -itd --restart=always -v /etc/localtime:/etc/localtime -v /home/mycontainers/myrabbit/data:/data --net mynetwork -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin rabbitmq:management
访问 http://myserverhost:15672 可以看到rabbit可视化界面
Maven依赖:
<!--消息队列-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置文件 application.yml:
server:
port: 8888
spring:
rabbitmq:
host: myserverhost
port: 5672
username: admin
password: admin
# 消息确认配置项
# 确认消息已发送到交换机(Exchange)
publisher-confirm-type: correlated
# 确认消息已发送到队列(Queue)
publisher-returns: true
消息队列的三个常用交换机
消息队列的大致流程为:生产者 -> 交换机 -> 消费者
消息由生产者发出,经过交换机被消费者接收,交换机的常用类型有三种:
- 直连交换机
- 主题交换机
- 扇形交换机
首先创建一个RabbitConstant的常量类:
package com.cc.config;
public class RabbitConstant {
/**
* 直连交换机
*/
public static final String DirectQueue = "DirectQueue";
public static final String DirectExchange = "DirectExchange";
public static final String DirectRouting = "DirectRouting";
public static final String LonelyDirectExchange = "LonelyDirectExchange";
/**
* 主题交换机
*/
// 绑定键
public final static String man = "topic.man";
public final static String woman = "topic.woman";
public static final String TopicExchange = "TopicExchange";
/**
* 扇形交换机
*/
public final static String FanoutA = "fanout.A";
public final static String FanoutB = "fanout.B";
public final static String FanoutC = "fanout.C";
public final static String FanoutExchange = "FanoutExchange";
/**
* 订单消息
*/
public final static String OrderQueue = "OrderQueue";
public final static String OrderExchange = "OrderExchange";
public final static String OrderRouting= "OrderRouting";
/**
* 订单延迟队列
*/
public final static String OrderDelayQueue = "OrderDelayQueue";
public final static String OrderDelayExchange = "OrderDelayExchange";
public final static String OrderDelayRouting = "OrderDelayRouting";
}
直连交换机
直连交换机是一对一的形式,消息只会被一个消费者接收,如果有多个消费者监听直连交换机的同一个队列,消息会以轮询的方式被消费,并不会重复消费。
消息生产者的配置类:
package com.cc.config.provider;
import com.cc.config.RabbitConstant;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 直连交换机
* @author cc
* @date 2021-11-27 10:20
*/
@Configuration
public class DirectRabbitConfig {
// 队列
@Bean
public Queue directQueue() {
// durable:是否持久化,默认false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在;暂存队列:当前连接有效
// exclusive:默认为false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除
// 一般设置队列持久化就可以
return new Queue(RabbitConstant.DirectQueue, true);
}
// Direct交换机
@Bean
DirectExchange directExchange() {
return new DirectExchange(RabbitConstant.DirectExchange, true, false);
}
// 绑定,将队列和交换机绑定,并设置用于匹配键:TestDirectRouting
@Bean
Binding bindingDirect() {
return BindingBuilder.bind(directQueue()).to(directExchange()).with(RabbitConstant.DirectRouting);
}
// 测试消息确认模块
@Bean
DirectExchange lonelyDirectExchange() {
return new DirectExchange(RabbitConstant.LonelyDirectExchange);
}
}
消费者消费类:
package com.cc.config.consumer;
import com.cc.config.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 直连交换机消费者
* @author cc
* @date 2021-11-27 10:55
*/
@Component
@RabbitListener(queues = RabbitConstant.DirectQueue)
public class DirectReceiver {
@RabbitHandler
public void process(Map testMsg) {
System.out.println("DirectReceiver消费者收到消息: " + testMsg.toString());
}
}
实际应用中,消息的生产者和消费者是分开来的,在示例中为了方便就集成到了一个项目里面,即我生产消息,我消费消息。
写接口测试:
private final RabbitTemplate rabbitTemplate;
public TestController(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
@GetMapping("/sendDirectMsg")
public String sendDirectMsg() {
Map<String, Object> map = generateMap("test message hello!");
// 将消息携带绑定值:TestDirectRouting发送到交换机TestDirectExchange
rabbitTemplate.convertAndSend(RabbitConstant.DirectExchange, RabbitConstant.DirectRouting, map);
return "ok";
}
测试效果是消息刚刚生产出来就被消费了。
这种消息属于一对一。
主题交换机
根据通配符来将消息发送给指定的队列
生产者配置类:
package com.cc.config.provider;
import com.cc.config.RabbitConstant;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 主题交换机
* @author cc
* @date 2021-11-27 10:20
*/
@Configuration
public class TopicRabbitConfig {
@Bean
public Queue firstQueue() {
return new Queue(RabbitConstant.man);
}
@Bean
public Queue secondQueue() {
return new Queue(RabbitConstant.woman);
}
@Bean
TopicExchange exchange() {
return new TopicExchange(RabbitConstant.TopicExchange);
}
/**
* 将firstQueue和TopicExchange绑定,而且绑定值为topic.man
* 这样只要是消息携带的路由器是topic.man,才会分发到该队列
*/
@Bean
Binding bindingExchangeMessage() {
return BindingBuilder.bind(firstQueue()).to(exchange()).with(RabbitConstant.man);
}
/**
* 将secondQueu和topicExchange绑定,而且绑定的键值为通配路由键topic.#
* 这样只要是消息携带的路由器是以topic.开头,就都会分发到该队列
*/
@Bean
Binding bindingExchangeMessage2() {
return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#");
}
}
在主题交换机中,我们配置了两个队列,一个队列响应我们指定的topic.man,一个队列响应满足topic.#规则的队列,关于匹配规则,有两个特殊字符:*和**#**,*表示任意一个词,#表示任意数量的词,当路由键为一个#时表示所有。
消费者消费类1:
package com.cc.config.consumer;
import com.cc.config.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 主题交换机消费者
* @author cc
* @date 2021-11-27 10:29
*/
@Component
@RabbitListener(queues = RabbitConstant.man)
public class TopicManReceiver {
@RabbitHandler
public void process(Map testMsg) {
System.out.println("TopicManReceiver消费者收到消息:" + testMsg.toString());
}
}
消费者消费类2:
package com.cc.config.consumer;
import com.cc.config.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 主题交换机消费者
* @author cc
* @date 2021-11-27 10:29
*/
@Component
@RabbitListener(queues = RabbitConstant.woman)
public class TopicTotalReceiver {
@RabbitHandler
public void process(Map testMsg) {
System.out.println("TopicTotalReceiver:" + testMsg.toString());
}
}
写接口测试:
@GetMapping("/sendTopicMsg1")
public String sendTopicMsg1() {
Map<String, Object> map = generateMap("message: M A N");
rabbitTemplate.convertAndSend(RabbitConstant.TopicExchange, RabbitConstant.man, map);
return "ok";
}
@GetMapping("/sendTopicMsg2")
public String sendTopicMsg2() {
Map<String, Object> map = generateMap("message: woman is all");
rabbitTemplate.convertAndSend(RabbitConstant.TopicExchange, RabbitConstant.woman, map);
return "ok";
}
测试结果为:
- 当我们发送路由键为topic.man的消息时,消费者1和2都消费到了,因为消费者2监听的主题是topic.#,topic.man符合该规则。
- 当我们发送路由键位topic.woman的消息时,只有消费者2消费到了,原因不用多说明了吧。
扇形交换机
多个队列绑定了同一个扇形交换机,发送到这个扇形交换机上的消息都会被多个队列消费到,即以广播的形式发送。
生产者配置类:
package com.cc.config.provider;
import com.cc.config.RabbitConstant;
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;
/**
* 扇形交换机
* @author cc
* @date 2021-11-27 10:48
*/
@Configuration
public class FanoutRabbitConfig {
/**
* 创建三个队列:fanout.A fanout.B fanout.C
* 将三个队列都绑定在交换机 fanoutExchange 上
* 因为是扇形交换机,路由键无需配置,配置也不起作用
*/
@Bean
public Queue queueA() {
return new Queue(RabbitConstant.FanoutA);
}
@Bean
public Queue queueB() {
return new Queue(RabbitConstant.FanoutB);
}
@Bean
public Queue queueC() {
return new Queue(RabbitConstant.FanoutC);
}
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange(RabbitConstant.FanoutExchange);
}
@Bean
Binding bindingExchangeA() {
return BindingBuilder.bind(queueA()).to(fanoutExchange());
}
@Bean
Binding bindingExchangeB() {
return BindingBuilder.bind(queueB()).to(fanoutExchange());
}
@Bean
Binding bindingExchangeC() {
return BindingBuilder.bind(queueC()).to(fanoutExchange());
}
}
消费者消费类1:
package com.cc.config.consumer;
import com.cc.config.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 扇形交换机消费者A
*/
@Component
@RabbitListener(queues = RabbitConstant.FanoutA)
public class FanoutReceiverA {
@RabbitHandler
public void process(Map testMsg) {
System.out.println("FanoutReceiverA消费者收到消息:" + testMsg.toString());
}
}
消费者消费类2:
package com.cc.config.consumer;
import com.cc.config.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 扇形交换机消费者A
*/
@Component
@RabbitListener(queues = RabbitConstant.FanoutB)
public class FanoutReceiverB {
@RabbitHandler
public void process(Map testMsg) {
System.out.println("FanoutReceiverB消费者收到消息:" + testMsg.toString());
}
}
消费者消费类3:
package com.cc.config.consumer;
import com.cc.config.RabbitConstant;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 扇形交换机消费者C
*/
@Component
@RabbitListener(queues = RabbitConstant.FanoutC)
public class FanoutReceiverC {
@RabbitHandler
public void process(Map testMsg) {
System.out.println("FanoutReceiverC消费者收到消息:" + testMsg.toString());
}
}
写接口测试:
@GetMapping("/sendFanoutMsg")
public String sendFanoutMsg() {
Map<String, Object> map = generateMap("message: testFanoutMsg");
rabbitTemplate.convertAndSend(RabbitConstant.FanoutExchange, null, map);
return "ok";
}
测试结果为所有的消费者都收到消息了,因为大家都注册了同一个交换机。
商品订单超时提醒的实现
rabbitmq里没有提供直接的超时提醒交换机,但是可以通过死信队列来变相实现,实现思路是这样的:
- 准备两个队列,订单延迟队列和订单队列
- 业务逻辑生成订单时,发送一条消息到订单延迟队列,如30分钟
- 30分钟后,该消息没有任何人消费,成为死信,成为死信后转发到订单队列
- 订单队列接收到转发过来的死信消息,很快就被消费者消费了
订单超时生产者配置类:
package com.cc.config.provider;
import com.cc.config.RabbitConstant;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* 订单超时消息
* 需有两个队列:订单延迟队列和订单队列
* 发送订单超时消息的流程为:
* 1. 发送消息到订单延迟队列,并指定过期时间
* 2. 订单延迟消息过期后成为死信,并转发到订单队列中
* 3. 订单队列收到订单延迟队列转发过来的消息,表示此时该订单消息已超时
* @author cc
* @date 2021-11-27 14:46
*/
@Configuration
public class OrderTimeoutRabbitConfig {
@Bean
public Queue orderDelayQueue() {
Map<String, Object> params = new HashMap<>();
// 声明,当队列里的消息过期后(即死信)转发到哪个Exchange
params.put("x-dead-letter-exchange", RabbitConstant.OrderExchange);
// 声明,死信转发时携带的routing-key
params.put("x-dead-letter-routing-key", RabbitConstant.OrderRouting);
return new Queue(RabbitConstant.OrderDelayQueue, true, false, false, params);
}
@Bean
public DirectExchange orderDelayExchange() {
return new DirectExchange(RabbitConstant.OrderDelayExchange);
}
@Bean
public Binding orderDelayBinding() {
return BindingBuilder.bind(orderDelayQueue()).to(orderDelayExchange()).with(RabbitConstant.OrderDelayRouting);
}
/**
* 要区分开订单延迟队列和订单队列
* 订单延迟队列的目的是为了实现延时
* 订单队列的目的是订单超时后的提醒
*/
@Bean
public Queue orderQueue() {
return new Queue(RabbitConstant.OrderQueue, true);
}
@Bean
public TopicExchange orderExchange() {
return new TopicExchange(RabbitConstant.OrderExchange);
}
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue()).to(orderExchange()).with(RabbitConstant.OrderRouting);
}
}
模拟一个订单类:
package com.cc.config.model;
import java.io.Serializable;
/**
* 订单类
* @author cc
* @date 2021-11-27 15:24
*/
public class Order implements Serializable {
/**
* 订单id
*/
private String orderId;
/**
* 订单状态
*/
private Integer orderStatus;
...
}
消费者消费类:
package com.cc.config.consumer;
import com.cc.config.RabbitConstant;
import com.cc.config.model.Order;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 直连交换机消费者
* @author cc
* @date 2021-11-27 10:55
*/
@Component
@RabbitListener(queues = RabbitConstant.OrderQueue)
public class OrderTimeoutReceiver {
@RabbitHandler
public void process(Order order) {
Integer status = order.getOrderStatus();
if (status == 0) {
System.out.println("订单未支付,关闭订单,释放库存");
} else {
System.out.println("订单已支付。");
}
System.out.println("收到订单超时的消息: " + order.toString());
}
}
写接口测试:
// 延时队列
@GetMapping("/sendDelayMsg")
public String sendDelayMsg() {
for (int i = 0; i < 100; i++) {
Order order = new Order();
order.setOrderId(UUID.randomUUID().toString().replace("-", ""));
order.setOrderStatus(new Random().nextInt(2));
rabbitTemplate.convertAndSend(RabbitConstant.OrderDelayExchange, RabbitConstant.OrderDelayRouting, order, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration(2000 + "");
return message;
}
});
}
return "ok";
}
测试结果为,消息发送2秒后被消费。
消费过程出现异常如何处理
因为消息被消费后默认是自动确认的,所以会出现在消费过程中出现异常后,业务没有处理成功,消息也没了的情况。
这时候我们就需要手动确认消息,当过程出现异常,就将消息放回队列重新消费。
我们可以指定需要手动确认的队列:
手动确认消息配置类:
package com.cc.config.consumer.confirm;
import com.cc.config.RabbitConstant;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 消息接收,手动确认
* @author cc
* @date 2021-11-27 14:33
*/
@Configuration
public class MessageListenerConfig {
private final CachingConnectionFactory connectionFactory;
private final MyAckReceiver myAckReceiver;
public MessageListenerConfig(CachingConnectionFactory connectionFactory, MyAckReceiver myAckReceiver) {
this.connectionFactory = connectionFactory;
this.myAckReceiver = myAckReceiver;
}
@Bean
public SimpleMessageListenerContainer simpleMessageListenerContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
container.setConcurrentConsumers(1);
container.setMaxConcurrentConsumers(1);
container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // RabbitMQ默认是自动确认,这里改为手动确认消息
// 设置一个队列
// container.setQueueNames("TestDirectQueue");
// 如果同时设置多个如下(前提是队列都是必须已经创建存在的):
// container.setQueueNames("TestDirectQueue", "TestDirectQueue2", "TestDirectQueue3");
// container.setQueueNames(RabbitConstant.DirectQueue, RabbitConstant.FanoutA); // 设置多个队列
container.setQueueNames(RabbitConstant.OrderQueue);
// 另一种设置队列的方法,如果使用这种情况,那么要设置多个,就使用addQueues
// container.setQueues(new Queue("TestDirectQueue", true));
// container.setQueues(new Queue("TestDirectQueue2", true));
// container.setQueues(new Queue("TestDirectQueue3", true));
container.setMessageListener(myAckReceiver);
return container;
}
}
手动确认消息类:
package com.cc.config.consumer.confirm;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
import java.util.Random;
/**
* 消息手动确认监听类,手动确认模式需要实现ChannelAwareMessageListener
*
* @author cc
* @date 2021-11-27 11:28
*/
@Component
public class MyAckReceiver implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
// if (RabbitConstant.DirectQueue.equals(message.getMessageProperties().getConsumerQueue())) {
// System.out.println("消费消息来自的队列名: " + message.getMessageProperties().getConsumerQueue());
// System.out.println("执行 DirectQueue 中的消息业务处理流程");
// }
//
// if (RabbitConstant.FanoutA.equals(message.getMessageProperties().getConsumerQueue())) {
// System.out.println("消费消息来自的队列名: " + message.getMessageProperties().getConsumerQueue());
// System.out.println("执行 FanoutA 中的消息业务处理流程");
// }
System.out.println("消费消息来自: " + message.getMessageProperties().getConsumerQueue());
if (new Random().nextInt(2) == 0) {
// 模拟异常
int a = 1 / 0;
}
channel.basicAck(deliveryTag, true);
} catch (Exception e) {
// 为true会重新放回队列
System.out.println("消息处理出现异常,将消息放回队列中");
// channel.basicReject(deliveryTag, true);
channel.basicNack(deliveryTag, true, true);
e.printStackTrace();
}
}
}
在示例中,我们用刚刚的订单超时的队列来做示范,在消费过程中模拟随机异常,测试结果为,当消费过程异常,消息被重新放入队列中重新消费,直到消费顺利,当然在实际场景中要增加重复消费次数的判断。
手动确认消息的几个知识点,以下知识点来源于第四章----SpringBoot+RabbitMQ发送确认和消费手动确认机制:
- channel.basicAck(long, boolean):确认收到消息,消息将被队列移除,false只确认当前consumer一个消息收到,true确认所有consumer获得的消息
- channel.basicNack(long, boolean, boolean):否定消息,第一个boolean表示一个consumer还是所有,第二个boolean表示是否重新回到队列,true重新入队
- channel.basicReject(long, boolean):拒绝消息,boolean为false表示不再重新入队,如果配置了死信队列则进入死信队列
- 当消息回滚到消息队列时,该消息不会回到队列尾部,而且仍在队列头部,这是消费者又会马上接收到这条消息,如果想要消息进入队尾,须确认后再次发送消息