RabbitMQ 作为目前应用相当广泛的消息中间件,在企业级应用、微服务应用中充当着重要的角色。特别是在一些典型的应用场景以及业务模块中具有重要的作用,比如业务服务模块解耦、异步通信、高并发限流、超时业务、数据延迟处理等。
这篇文章带领大家使用RabbitMQ实现延时队列
工欲善其事,必先利其器,接触一个新技术之前,肯定要先安装环境和工具,本篇文章不提供安装教程,下方提供了一个安装RabbitMQ的博客:
RabbitMQ安装教程
安装成功之后RabbitMQ的运行界面就是这样的
这样我们的项目环境就搭建成功了。
延迟队列,也叫“延时队列”,顾名思义,其实就是“生产者生产消息,消息进入队列之后,并不会立即被指定的消费者所消费,而是会延时一段指定的时间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),并且设置该消息的过期时间,等过期时间到达时再取出消费即可。
下面我们就用延时队列来实现,某个时间段过后取消未付款的订单
<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>
3.配置文件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
/**
* 作者: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.配置队列
在config下面新建DelayRabbitConfig.java,将它作为一个配置类使用(copy之前记得看注释)
@Configuration
@Slf4j
public class DelayRabbitConfig {
}
5.1 下面我们新建常量来命名立即消费队列和延时消费队列
// 延迟队列 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";
5.2 创建立即消费队列和延时队列
//创建一个延时队列
@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);
}
5.3 新建立即消费队列的交换机和延时队列的交换机
//延迟交换机
@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);
}
5.4 把新建的队列和交换机进行绑定
// 把延时队列和 订单延迟交换的exchange进行绑定
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(delayOrderQueue()).to(orderDelayExchange()).with(ORDER_DELAY_ROUTING_KEY);
}
// 把立即队列和 立即交换的exchange进行绑定
@Bean
public Binding orderBinding() {
// TODO 如果要让延迟队列之间有关联,这里的 routingKey 和 绑定的交换机很关键
return BindingBuilder.bind(orderQueue()).to(orderTopicExchange()).with(ORDER_ROUTING_KEY);
}
/**
* 作者: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操作对象,第四个参数为消息
7.在config包下面新建消费者:DelayReceiver.java
// 接收者 --消费者
@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的实现过程
@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里面看看我们的队列和交换机
交换机
队列
一分钟后观察控制台
可以看到未支付的订单已经改变状态,至此我们实现了一个简单的超时订单取消支付,后面可以根据自己的项目需求不断添加改变
本文可能在许多rabbitmq的许多概念没有说的特别清楚,但是都是自己看了这么多文章自己的理解,如有问题欢迎指出!!