测试前言
RabbitMQ 作为目前应用相当广泛的消息中间件,在企业级应用、微服务应用中充当着重要的角色。特别是在一些典型的应用场景以及业务模块中具有重要的作用,比如业务服务模块解耦、异步通信、高并发限流、超时业务、数据延迟处理等。
这篇文章带领大家使用RabbitMQ实现延时队列
1.搭建项目环境
工欲善其事,必先利其器,接触一个新技术之前,肯定要先安装环境和工具,本篇文章不提供安装教程,不清楚RabbitMq安装的请看我的另一篇文章《最简单的RabbitMQ消息队列搭建(windows环境下安装)》,安装成功后启动RabbitMq服务
安装成功之后RabbitMQ,在浏览器中输入地址查看:http://127.0.0.1:15672/,运行界面就是这样的
这样我们的项目环境就搭建成功了。
2.延时队列–实现思路
延迟队列,也叫“延时队列”,顾名思义,其实就是“生产者生产消息,消息进入队列之后,并不会立即被指定的消费者所消费,而是会延时一段指定的时间TTL(Time To Live),最终才被消费者消费。
RabbitMQ本身是不支持延时队列,而是同过二个特性来实现的:
- Time To Live(TTL)
RabbitMQ可以针对Queue设置x-expires 或者 针对Message设置 x-message-ttl,来控制消息的生存时间,如果超时(两者同时设置以最先到期的时间为准),则消息变为dead letter(死信)
RabbitMQ针对队列中的消息过期时间有两种方法可以设置。
A: 通过队列属性设置,队列中所有消息都有相同的过期时间。
B: 对消息进行单独设置,每条消息TTL可以不同。
如果同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就成为dead letter(死信) - Dead Letter Exchanges(DLX)
RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter(死信),则按照这两个参数重新路由转发到指定的队列。
x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange
x-dead-letter-routing-key:出现dead letter之后将dead letter重新按照指定的routing-key发送路由
可能说这么多还是看不太懂,我准备了一张图
延时队列的执行流程就是图中所示,介绍完延时队列的概念之后,给大家举一个在项目中常见的场景:
用户创建下单记录之后,会对其进行付款,付款成功之后,该条记录将变为已支付并且有效,否则的话,一旦过了指定的时间,即超时了,则该记录将置为无效,并且不能被用于后续的业务逻辑
可能有人用过定时器(Timer)也可以实现类似的功能,但是定时器不能精准的知道哪些需要执行任务,查询范围太大,太浪费性能。
使用rabbitmap,我们只用把需要把的某个订单放入消息中间去(message),并且设置该消息的过期时间,等过期时间到达时再取出消费即可。
下面我们就用延时队列来实现,某个时间段过后取消未付款的订单
3.使用springboot+RabbitMq进行测试
1、新建SpringBoot项目如下:
2、在pom.xml中引入项目需要的jar包
pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>rabbitmq-order-delay</artifactId> <version>0.0.1-SNAPSHOT</version> <name>rabbitmq-order-delay</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--rabbitmq--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
3、配置文件application.properties
application.properties
#本机ip地址,一般装在本机直接使用localhost,若是虚拟机,则使用虚拟机的ip地址 spring.rabbitmq.host=localhost # 端口号 spring.rabbitmq.port=5672 # rabbitmq的用户信息,默认都为guest spring.rabbitmq.username=guest spring.rabbitmq.password=guest
4、在与springboot启动类同级新建config和pojo和controller包,新建实体类:Order
package com.example.rabbitmqorderdelay.pojo; import lombok.Data; import java.io.Serializable; /** * 作者:LSH */ @Data public class Order implements Serializable { private static final long serialVersionUID = -2221214252163879885L; private String orderId; // 订单id private Integer orderStatus; // 订单状态 0:未支付,1:已支付,2:订单已取消 private String orderName; // 订单名字 }
5、配置队列
(1)在config下面新建DelayRabbitConfig.java,将它作为一个配置类使用(copy之前记得看注释)
package com.example.rabbitmqorderdelay.config; import lombok.extern.slf4j.Slf4j; 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; @Configuration @Slf4j public class DelayRabbitConfig { // 延迟队列 TTL 名称 private static final String ORDER_DELAY_QUEUE = "order.delay.queue"; // DLX,dead letter发送到的 exchange // 延时消息就是发送到该交换机的 public static final String ORDER_DELAY_EXCHANGE = "order.delay.exchange"; // routing key 名称 // 具体消息发送在该 routingKey 的 public static final String ORDER_DELAY_ROUTING_KEY = "order_delay"; //立即消费的队列名称 public static final String ORDER_QUEUE_NAME = "order.queue"; // 立即消费的exchange public static final String ORDER_EXCHANGE_NAME = "order.exchange"; //立即消费 routing key 名称 public static final String ORDER_ROUTING_KEY = "order"; /** * 创建一个延时队列 */ @Bean public Queue delayOrderQueue() { Map<String, Object> params = new HashMap<>(); // x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称, params.put("x-dead-letter-exchange", ORDER_EXCHANGE_NAME); // x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。 params.put("x-dead-letter-routing-key", ORDER_ROUTING_KEY); return new Queue(ORDER_DELAY_QUEUE, true, false, false, params); } /** * 创建一个立即消费队列 */ @Bean public Queue orderQueue() { // 第一个参数为queue的名字,第二个参数为是否支持持久化 return new Queue(ORDER_QUEUE_NAME, true); } /** * 延迟交换机 */ @Bean public DirectExchange orderDelayExchange() { // 一共有三种构造方法,可以只传exchange的名字, 第二种,可以传exchange名字,是否支持持久化,是否可以自动删除, // 第三种在第二种参数上可以增加Map,Map中可以存放自定义exchange中的参数 // new DirectExchange(ORDER_DELAY_EXCHANGE,true,false); return new DirectExchange(ORDER_DELAY_EXCHANGE); } /** * 立即消费交换机 */ @Bean public TopicExchange orderTopicExchange() { return new TopicExchange(ORDER_EXCHANGE_NAME); } /** * 把延时队列和 订单延迟交换的exchange进行绑定 * @return */ @Bean public Binding dlxBinding() { return BindingBuilder.bind(delayOrderQueue()).to(orderDelayExchange()).with(ORDER_DELAY_ROUTING_KEY); } /** * 把立即队列和 立即交换的exchange进行绑定 * @return */ @Bean public Binding orderBinding() { // TODO 如果要让延迟队列之间有关联,这里的 routingKey 和 绑定的交换机很关键 return BindingBuilder.bind(orderQueue()).to(orderTopicExchange()).with(ORDER_ROUTING_KEY); } }
(2)在config包下面新建生产者:DelaySender.java, 声明它是一个工具类,这里我们日志为了简化流程使用了springboot自带的日志
package com.example.rabbitmqorderdelay.config; import com.example.rabbitmqorderdelay.pojo.Order; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Date; /** * 作者:LSH * 日期:2019/12/18 21:44 * 生产者 生产消息 */ @Component @Slf4j public class DelaySender { // AMQP 高级消息队列协议 @Autowired private AmqpTemplate amqpTemplate; public void sendDelay(Order order) { log.info("【订单生成时间】" + new Date().toString() +"【1分钟后检查订单是否已经支付】" + order.toString() ); this.amqpTemplate.convertAndSend(DelayRabbitConfig.ORDER_DELAY_EXCHANGE, DelayRabbitConfig.ORDER_DELAY_ROUTING_KEY, order, message -> { // 如果配置了 params.put("x-message-ttl", 5 * 1000); 那么这一句也可以省略,具体根据业务需要是声明 Queue 的时候就指定好延迟时间还是在发送自己控制时间 message.getMessageProperties().setExpiration(1 * 1000 * 60 + ""); return message; }); } } 这里声明的amqpTemplate接口,这个接口包含了发送和接收消息的一般操作,换种说法,它不是某个实现所专有的,所以AMQP存在于名称里。这个接口的实现与AMQP协议的实现紧密关联。 this.amqpTemplate.convertAndSend的第一个参数为延迟交换机的名称,第二个为延时消费routing-key,第三个参数为order操作对象,第四个参数为消息
(3)在config包下面新建消费者:DelayReceiver.java
package com.example.rabbitmqorderdelay.config; import com.example.rabbitmqorderdelay.pojo.Order; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.messaging.Message; import org.springframework.stereotype.Component; import java.util.Date; // 接收者 --消费者 @Component @Slf4j public class DelayReceiver { @RabbitListener(queues = {DelayRabbitConfig.ORDER_QUEUE_NAME}) public void orderDelayQueue(Order order, Message message, Channel channel) { log.info("###########################################"); log.info("【orderDelayQueue 监听的消息】 - 【消费时间】 - [{}]- 【订单内容】 - [{}]", new Date(), order.toString()); if (order.getOrderStatus() == 0) { order.setOrderStatus(2); log.info("【该订单未支付,取消订单】" + order.toString()); } else if (order.getOrderStatus() == 1) { log.info("【该订单已完成支付】"); } else if (order.getOrderStatus() == 2) { log.info("【该订单已取消】"); } } }
在这个类中我们定义了一个普通方法,可能你会很纳闷为什么这个普通方法为什么可以进行接收消息,主要还是这个注解: @RabbitListener,下面给大家简单了解下这个注解的作用,@RabbitListener注解的方法所在的类首先是一个bean,因此,实现 BeanPostProcessor接口对每一个初始化完成的bean进行处理。比如上面 DelayRabbitConfig.ORDER_QUEUE_NAME所在的方法就是一个Bean
- 遍历bean中由用户自己定义的所有的方法,找出其中添加了@RabbitListener注解的方法
- 读取上面找出的所有方法上@RabbitListener注解中的值,并为每一个方法创建一个RabbitListenerEndpoint,保存在RabbitListenerEndpointRegistrar类中
- 在所有的bean都初始化完成,即所有@RabbitListener注解的方法都创建了endpoint之后,由我们配置的RabbitListenerContainerFactory将每个endpoint创建MessageListenerContainer
- 最后创建上面的MessageListenerContainer
- 至此,全部完成,MessageListenerContainer启动后将能够接受到消息,再将消息交给它的MessageListener处理消息
(4)最后在controller包下面新建TestController.java 进行测试
package com.example.rabbitmqorderdelay.controller; import com.example.rabbitmqorderdelay.config.DelaySender; import com.example.rabbitmqorderdelay.pojo.Order; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { @Autowired private DelaySender delaySender; @GetMapping("/sendDelay") public Object sendDelay() { Order order1 = new Order(); order1.setOrderStatus(0); order1.setOrderId("123321123"); order1.setOrderName("波音747飞机"); Order order2 = new Order(); order2.setOrderStatus(1); order2.setOrderId("2345123123"); order2.setOrderName("豪华游艇"); Order order3 = new Order(); order3.setOrderStatus(2); order3.setOrderId("983676"); order3.setOrderName("小米alpan阿尔法"); delaySender.sendDelay(order1); delaySender.sendDelay(order2); delaySender.sendDelay(order3); return "test--ok"; } }
成功启动项目,打开浏览器,输入: http://localhost:8080/sendDelay
打开RabbitMq的管理页面http://127.0.0.1:15672/#/,可以看到交换机
看到队列情况
一分钟后观察控制台
可以看到未支付的订单已经改变状态,至此我们实现了一个简单的超时订单取消支付,后面可以根据自己的项目需求不断添加改变
总结
本文可能在许多rabbitmq的许多概念没有说的特别清楚,但是都是自己看了这么多文章自己的理解,如有问题欢迎指出!!