概述
在事情有一定时效性的时候:比如
实现一个在提交订单后的15分钟内,如果没有完成支付,系统关闭订单。有哪些可行的方案?
方案
- 使用定时任务轮询订单表,查询出订单创建了15分钟以上并且未支付的订单,如果有查询出此类订单则执行关闭。
缺点: 使用定时任务轮询订单表,查询出订单创建了15分钟以上并且未支付的订单,如果有查询出此类订单则执行关闭。
- 提交订单时开启一个新线程,而新线程直接休眠15分钟,休眠结束后开始执行订单关闭
缺点:如果在线程休眠时,重启了整个服务,那么会怎样呢?
- 使用延时消息队列
缺点:需要额外部署消息中间件
综上考虑:使用延时消息队列则成为最佳选择,消息延时发布之后,保存在消息中间件中,在15分钟后才会正式发布至队列,延时队列监听器在15分钟后监听到消息时,才开始执行,而这期间,即使项目重启也没有关系。
实现思路
本身在RabbitMQ
中是未直接提供延时队列功能的,但可以使用TTL(Time-To-Live,存活时间)
和DLX(Dead-Letter-Exchange,死信队列交换机)
的特性实现延时队列的功能。
Time-To-Live Extensions(存活时间)
RabbitMQ允许我们为消息或者队列设置TTL(time to live),也就是过期时间。TTL表明了一条消息可在队列中存活的最大时间,单位为毫秒。也就是说,当某条消息被设置了TTL或者当某条消息进入了设置了TTL的队列时,这条消息会在经过TTL秒后“死亡”,成为Dead Letter。如果既配置了消息的TTL,又配置了队列的TTL,那么较小的那个值会被取用。更多资料请查阅官方文档。
简单介绍下rabbitmq执行的流程
①:生产者将消息(msg)和路由键(routekey)发送指定的交换机(exchange)上
②:交换机(exchange)根据路由键(routekey)找到绑定自己的队列(queue)并把消息给它
③:队列(queue)再把消息发送给监听它的接受者 (order)
Dead Letter Exchange(死信交换)
刚才提到了,被设置了TTL的消息在过期后会成为Dead Letter。其实在RabbitMQ中,一共有三种消息的“死亡”形式:
- 消息被拒绝。通过调用basic.reject或者basic.nack并且设置的requeue参数为false。
- 消息因为设置了TTL而过期。
- 消息进入了一条已经达到最大长度的队列。
如果队列设置了Dead Letter Exchange(DLX),那么这些Dead Letter就会被重新publish到Dead Letter Exchange,通过Dead Letter Exchange路由到其他队列。更多资料请查阅官方文档。
所以,通过TTL
和DLX
的特性可以模拟实现延时队列的功能。当队列中的消息超时成为死信后,会把消息死信重新发送到配置好的交换机中,然后分发到真实的消费队列。故简单来说,我们可以创建2个队列,一个队列用于发送消息,一个队列用于消息过期后的转发的目标队列。
两种机制结合模拟出延时队列来
①:生产者将消息(msg)和路由键发送指定的死信交换机上
②:死信交换机根据路由键找到绑定自己的死信队列并把消息给它
③:消息(msg)到期死亡变成死信转发给死信接收交换机
④:死信接收交换机根据路由键找到绑定自己的死信接收队列并把消息给它
⑤:死信接收队列再把消息发送给监听它的接受者
工具准备
- 所需工具为 erlang和RabbitMQ
- 安装工具请参考win10安装rabbitmq
springboot集成RabbitMQ
安装完工具后就可以去创建springboot项目集成RabbitMQ工具来实现延时队列
创建一个普通的springboot项目
<!-- 在pom文件中加入启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在 application.yml中加入配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest #当前登录的rabbitmq的账号 默认的账户的就是guest
password: guest #密码 默认的账户的密码页数guest
如果是application.properties
格式的配置文件
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
配置队列
package com.weisen.springbootrabbit.config;
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;
/**
* 作者:weisen
* 日期:2019/12/19 11:07
*/
@Configuration
public class DelayRabbitConfig {
/**
* 延迟队列 TTL 名称
*/
private static final String ORDER_DELAY_QUEUE = "user.order.delay.queue";
/**
* DLX,dead letter发送到的 exchange
* 延时消息就是发送到该交换机的
*/
public static final String ORDER_DELAY_EXCHANGE = "user.order.delay.exchange";
/**
* routing key 名称 路由键
* 具体消息发送在该 routingKey 的
*/
public static final String ORDER_DELAY_ROUTING_KEY = "order_delay";
public static final String ORDER_QUEUE_NAME = "user.order.queue";
public static final String ORDER_EXCHANGE_NAME = "user.order.exchange";
public static final String ORDER_ROUTING_KEY = "order";
/**
* 死信接收队列
*/
@Bean
public Queue orderQueue() {
return new Queue(ORDER_QUEUE_NAME, true);
}
/**
* 死信交换机
* 将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。
* 符号“#”匹配一个或多个词,符号“*”匹配不多不少一个词。因此“audit.#”能够匹配到“audit.irs.corporate”,但是“audit.*” 只会匹配到“audit.irs”。
**/
@Bean
public TopicExchange orderTopicExchange() {
return new TopicExchange(ORDER_EXCHANGE_NAME);
}
/**
* 死信接收队列绑定交换机
*/
@Bean
public Binding orderBinding() {
// TODO 如果要让延迟队列之间有关联,这里的 routingKey 和 绑定的交换机很关键
return BindingBuilder.bind(orderQueue()).to(orderTopicExchange()).with(ORDER_ROUTING_KEY);
}
/**
* 延迟队列配置(死信队列)
* <p>
* 1、params.put("x-message-ttl", 5 * 1000);
* 第一种方式是直接设置 Queue 延迟时间 但如果直接给队列设置过期时间,这种做法不是很灵活,(当然二者是兼容的,默认是时间小的优先)
* 2、rabbitTemplate.convertAndSend(book, message -> {
* message.getMessageProperties().setExpiration(2 * 1000 + "");
* return message;
* });
* 第二种就是每次发送消息动态设置延迟时间,这样我们可以灵活控制
**/
@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);
}
/**
* 需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。
* 这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “dog”,则只有被标记为“dog”的消息才被转发,
* 不会转发dog.puppy,也不会转发dog.guard,只会转发dog。
* @return DirectExchange
*/
@Bean
public DirectExchange orderDelayExchange() {
return new DirectExchange(ORDER_DELAY_EXCHANGE);
}
/**
* 延迟队列绑定交换机
*/
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(delayOrderQueue()).to(orderDelayExchange()).with(ORDER_DELAY_ROUTING_KEY);
}
}
创建实体
package com.weisen.springbootrabbit.pojo;
import lombok.Data;
import java.io.Serializable;
/**
* 作者:weisen
* 日期:2019/12/19 11:09
*/
@Data
public class Order implements Serializable {
private static final long serialVersionUID = -6805548835717079337L;
private String orderId; // 订单id
private Integer orderStatus; // 订单状态 0:未支付,1:已支付,2:订单已取消
private String orderName; // 订单名字
}
创建发送者
package com.weisen.springbootrabbit.utils;
import com.weisen.springbootrabbit.config.DelayRabbitConfig;
import com.weisen.springbootrabbit.pojo.Order;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 发送者
* 作者:weisen
* 日期:2019/12/19 11:14
*/
@Component
public class DelaySender {
@Autowired
private AmqpTemplate amqpTemplate;
public void sendDelay(Order order) {
System.out.println("【订单生成时间】" + 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;
});
}
}
接收者
package com.weisen.springbootrabbit.utils;
import com.rabbitmq.client.Channel;
import com.weisen.springbootrabbit.config.DelayRabbitConfig;
import com.weisen.springbootrabbit.pojo.Order;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 接收者
* 作者:weisen
* 日期:2019/12/19 11:10
*/
@Component
public class DelayReceiver {
@RabbitListener(queues = {DelayRabbitConfig.ORDER_QUEUE_NAME})
public void orderDelayQueue(Order order, Message message, Channel channel) {
System.out.println("###########################################");
System.out.println("【orderDelayQueue 监听的消息】 - 【消费时间】 - ["
+new Date()+"]- 【订单内容】 - ["+order.toString()+"]");
if(order.getOrderStatus() == 0) {
order.setOrderStatus(2);
System.out.println("【该订单未支付,取消订单】" + order.toString());
} else if(order.getOrderStatus() == 1) {
System.out.println("【该订单已完成支付】");
} else if(order.getOrderStatus() == 2) {
System.out.println("【该订单已取消】");
}
System.out.println("###########################################");
}
}
创建测试
package com.weisen.springbootrabbit.controller;
import com.weisen.springbootrabbit.pojo.Order;
import com.weisen.springbootrabbit.utils.DelaySender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 作者:weisen
* 日期:2019/12/19 11:17
*/
@RestController
public class TestController {
@Autowired
private DelaySender delaySender;
@GetMapping("/sendDelay")
public Object sendDelay() {
Order order1 = new Order();
order1.setOrderStatus(0);
order1.setOrderId("123456");
order1.setOrderName("小米6");
Order order2 = new Order();
order2.setOrderStatus(1);
order2.setOrderId("456789");
order2.setOrderName("小米8");
delaySender.sendDelay(order1);
delaySender.sendDelay(order2);
return "ok";
}
}
查看结果
运行 localhost:8080/sendDelay
执行测试
【订单生成时间】Thu Dec 19 11:24:55 CST 2019【1分钟后检查订单是否已经支付】Order(orderId=123456, orderStatus=0, orderName=小米6)
【订单生成时间】Thu Dec 19 11:24:55 CST 2019【1分钟后检查订单是否已经支付】Order(orderId=456789, orderStatus=1, orderName=小米8)
###########################################
【orderDelayQueue 监听的消息】 - 【消费时间】 - [Thu Dec 19 11:25:55 CST 2019]- 【订单内容】 - [Order(orderId=123456, orderStatus=0, orderName=小米6)]
【该订单未支付,取消订单】Order(orderId=123456, orderStatus=2, orderName=小米6)
###########################################
###########################################
【orderDelayQueue 监听的消息】 - 【消费时间】 - [Thu Dec 19 11:25:55 CST 2019]- 【订单内容】 - [Order(orderId=456789, orderStatus=1, orderName=小米8)]
【该订单已完成支付】
###########################################
rabbitmq的四种EXchange
在message到达Exchange后,Exchange会根据route规则进入对应的Queue中,message可能进入一个Queue也可能进入对应多个Queue,至于进入哪个Queue或者是说哪个Queue都不进入,这要依据ExChange的ExchangeType和Exchange所绑定的路由规则,实现AMQP0.9.1协议的RabbitMQ Broker提供了四种ExChangeType。
这四种ExchangeType分别是Direct exchange
,Fanout exchange
,Topic exchange
和Headers exchange
。
Direct Exchange:
DirectExchange是RabbitMQ Broker的默认Exchange,它有一个特别的属性对一些简单的应用来说是非常有用的,在使用这个类型的Exchange时,可以不必指定routing key的名字,在此类型下创建的Queue有一个默认的routing key,这个routing key一般同Queue同名。
Fanout Exchange:
使用这种类型的Exchange,会忽略routing key的存在,直接将message广播到所有的Queue中。
Topic Exchange:
Topic Exchange是根据routing key和Exchange的类型将message发送到一个或者多个Queue中,我们经常拿他来实现各种publish/subscribe,即发布订阅,
Headers Exchange:
Headers Exchange不同于上面三种Exchange,它是根据Message的一些头部信息来分发过滤Message,忽略routing key的属性,如果Header信息和message消息的头信息相匹配,那么这条消息就匹配上了。
借鉴
本文关于RabbitMQ的四种ExChange 引用于博客:RabbitMQ的四种ExChange
本文核心springboot集成rabbitmq引用于博客:SpringBoot使用RabbitMQ延时队列并加上了一些自己的理解