RabbitMQ 作为目前应用相当广泛的消息中间件,在企业级应用、微服务应用中充当着重要的角色。特别是在一些典型的应用场景以及业务模块中具有重要的作用,比如业务服务模块解耦、异步通信、高并发限流、超时业务、数据延迟处理等。
这篇文章带领大家使用RabbitMQ实现延时队列
工欲善其事,必先利其器,接触一个新技术之前,肯定要先安装环境和工具,本篇文章不提供安装教程,不清楚RabbitMq安装的请看我的另一篇文章《最简单的RabbitMQ消息队列搭建(windows环境下安装)》,安装成功后启动RabbitMq服务
安装成功之后RabbitMQ,在浏览器中输入地址查看:http://127.0.0.1:15672/,运行界面就是这样的
这样我们的项目环境就搭建成功了。
延迟队列,也叫“延时队列”,顾名思义,其实就是“生产者生产消息,消息进入队列之后,并不会立即被指定的消费者所消费,而是会延时一段指定的时间TTL(Time To Live),最终才被消费者消费。
RabbitMQ本身是不支持延时队列,而是同过二个特性来实现的:
可能说这么多还是看不太懂,我准备了一张图
延时队列的执行流程就是图中所示,介绍完延时队列的概念之后,给大家举一个在项目中常见的场景:
用户创建下单记录之后,会对其进行付款,付款成功之后,该条记录将变为已支付并且有效,否则的话,一旦过了指定的时间,即超时了,则该记录将置为无效,并且不能被用于后续的业务逻辑
可能有人用过定时器(Timer)也可以实现类似的功能,但是定时器不能精准的知道哪些需要执行任务,查询范围太大,太浪费性能。
使用rabbitmap,我们只用把需要把的某个订单放入消息中间去(message),并且设置该消息的过期时间,等过期时间到达时再取出消费即可。
下面我们就用延时队列来实现,某个时间段过后取消未付款的订单
1、新建SpringBoot项目如下:
2、在pom.xml中引入项目需要的jar包
pom.xml如下:
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.2.2.RELEASE
com.example
rabbitmq-order-delay
0.0.1-SNAPSHOT
rabbitmq-order-delay
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-amqp
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.boot
spring-boot-maven-plugin
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 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
(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的许多概念没有说的特别清楚,但是都是自己看了这么多文章自己的理解,如有问题欢迎指出!!