前言
在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢?
业务场景
我们以订单功能为例说明下:生成订单后一段时间不支付订单会自动关闭。最简单的想法是设置定时任务轮询,但是每个订单的创建时间不一样,定时任务的规则无法设定,如果将定时任务执行的间隔设置的过短,太影响效率。还有一种想法,在用户进入订单界面的时候,判断时间执行相关操作。
一、定时任务
一般情况下,最不推荐的方式就是关单方式就是定时任务方式,原因我们可以看下面的图来说明
我们假设,关单时间为下单后10分钟,定时任务间隔也是10分钟;通过上图我们看出,如果在第1分钟下单,在第20分钟的时候才能被扫描到执行关单操作,这样误差达到10分钟,这在很多场景下是不可接受的,另外需要频繁扫描主订单号造成网络IO和磁盘IO的消耗,对实时交易造成一定的冲击,所以PASS
二、RabbitMQ延迟队列
我们在下单后,往MQ投递一个消息,设置其有效期为30分钟,在不设置对应队列的消费者的情况下,该消息将一直不被消费,那么30分钟后,该消息过期会被投递到死信队列,由死信消费者消费,我们就可以在死信消费者根据订单id执行相应的业务逻辑
- MQ的配置:
#配置virtual-host虚拟主机
spring.rabbitmq.virtual-host=test_order_close
#ip地址
spring.rabbitmq.host=127.0.0.1
#用户名 密码
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#连接端口号
spring.rabbitmq.port=5672
#死信队列
test.dlx.exchange=test_order_dlx_exchange
test.dlx.queue=test_order_dlx_queue
test.dlx.routingKey=dlx
##备胎交换机
test.order.exchange=test_order_exchange
test.order.queue=test_order_queue
test.order.routingKey=test.order
- 死信的config类
@Component
public class DeadLetterMQConfig {
/**
* 订单交换机
*/
@Value("${test.order.exchange}")
private String orderExchange;
/**
* 订单队列
*/
@Value("${test.order.queue}")
private String orderQueue;
/**
* 订单路由key
*/
@Value("${test.order.routingKey}")
private String orderRoutingKey;
/**
* 死信交换机
*/
@Value("${test.dlx.exchange}")
private String dlxExchange;
/**
* 死信队列
*/
@Value("${test.dlx.queue}")
private String dlxQueue;
/**
* 死信路由
*/
@Value("${test.dlx.routingKey}")
private String dlxRoutingKey;
/**
* 声明死信交换机
*/
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange(dlxExchange);
}
/**
* 声明死信队列
*/
@Bean
public Queue dlxQueue() {
return new Queue(dlxQueue);
}
/**
* 声明订单业务交换机
*/
@Bean
public DirectExchange orderExchange() {
return new DirectExchange(orderExchange);
}
/**
* 声明订单队列 核心操作一
*/
@Bean
public Queue orderQueue() {
Map<String, Object> arguments = new HashMap<>(2);
// 绑定我们的死信交换机
arguments.put("x-dead-letter-exchange", dlxExchange);
// 绑定我们的路由key
arguments.put("x-dead-letter-routing-key", dlxRoutingKey);
return new Queue(orderQueue, true, false, false, arguments);
}
/**
* 绑定订单队列到订单交换机
*/
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue()).to(orderExchange()).with(orderRoutingKey);
}
/**
* 绑定死信队列到死信交换机
*/
@Bean
public Binding binding() {
return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with(dlxRoutingKey);
}
}
- Controller
@GetMapping("/addOrder")
public String addOrder(){
String orderId=System.currentTimeMillis()+"";
OrderEntity orderEntity=new OrderEntity("订单过期测试",orderId,0);
int result= orderMapper.addOrder(orderEntity);
if(result<=0){
return "fail";
}
rabbitTemplate.convertAndSend(orderExchange,orderRoutingKey,orderId,messagePostProcessor());
return "success";
}
//处理待发送消息
private MessagePostProcessor messagePostProcessor(){
return new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//设置有效期20秒
message.getMessageProperties().setExpiration("20000");
return message;
}
};
}
- 死信消费者
public class OrderDlxConsumer {
@Autowired
private OrderMapper orderMapper;
//监听死信队列
@RabbitListener(queues = "test_order_dlx_queue")
public void orderConsumer(String orderId) {
System.out.println("死信队列获取消息:" + orderId);
if (StringUtils.isEmpty(orderId)) {
return;
}
//根据id查询
OrderEntity orderEntity = orderMapper.getOrder(orderId);
if (null == orderEntity) {
return;
}
//获取状态
Integer orderStatus=orderEntity.getOrderStatus();
//判断未支付 , 关闭订单
if(0==orderStatus){
orderMapper.updateStatus(orderId,2);
}
}
}
- 测试