取消超时订单及延迟处理方案

  1. 使用场景
  2. 方案
  3. 优化

1.使用场景

12306订单30分钟自动取消?

淘宝订单超过2小时自动取消?

美团外卖订单超过30分钟自动取消?

抢购如何处理?被动更新 + crond 主动更新两种方式,因为是抢购,下单扣库存,5分钟不支付马上过期恢复库存。

订单支付的时候再去校验时间是否过期,查询校验一次、订单支付校验一次

另请注意,请判断好支付完成回调的验证,因为用户下单后,20几分钟后再点击付款,再到支付页面停留,时间已经超过30分钟,然后支付成功回调时请注意判断验证回调的信息

订单如果一直在支付页面,不支付呢?如果支付完成回跳,订单恰好过期了,直接将订单设置为待退款状态,然后走另一个定时任务完成退款。微信支付可以设置订单超时自动关闭,那么一直不支付的话,调起支付会提示订单已关闭。

2.方案

被动过期策略

方案1:

在每次查询这个订单时候检查过期,被动过期。

比如,查询订单细节时,再去检查是否过期然后再处理。

当然,如果这条数据不被访问可能永远不会过期,直到有人访问它。

有点像薛定谔的猫,在你打开盒子(检查订单)之后,才知道它是否过期。所以叫被动过期。

方案2:redis

在创建订单时候,将订单id作为key存入redis,过期时间为30分钟。

用户查询待支付订单列表时,检查id是否还存于redis,不存在则关闭订单。发起支付时同理关闭订单,订单支付成功回调时则将订单置为退款状态。

redis主动过期策略:redis实现延时队列的两种方式redis实现延时队列的两种方式_暮色里de白雪檐的博客-CSDN博客_redis实现延时队列

主动过期策略

方案3:数据库轮训、定时任务:springboot的自带的schedule的注解、用quartz,定时器

写个定时任务,每一分钟查询未支付订单,如果其时间超过大于等于创建订单时间30分钟,则关闭订单

优点:实现简单、无技术难点、异常恢复、支持分布式/集群环境

缺点:性能低

方案4:java延迟队列delayQueue

优点:实现简单、性能较好

缺点:异常恢复困难、分布式/集群实现困难

1.知识点

delayQueue相关api:DelayQueue是java.util.concurrent中提供的一个类。

DelayQueue小结

DelayQueue是一个有序的无界BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象在到期时才能从队列中取走。

DelayQueue只能添加实现了Delayed接口的对象,不能将null元素放置到这种队列中。

BlockingQueue中add,offer,put方法区别

  1. add:将指定的元素插入到此队列中,在成功时返回 true,如果当前没有可用空间,则抛出 IllegalStateException,该方式为非阻塞添加。
  2. offer:将指定元素插入到此队列的尾部(如果立即可行且不会超出此队列的容量),在成功时返回 true,如果此队列已满,则返回 false,此方法通常要优于 add 方法,该方式为非阻塞添加。
  3. put:将指定元素插入到此队列的尾部,该方式为阻塞添加,则等待空间变得可用。
  4. take:出队,将符合条件的元素取出,take()方法阻塞等待,有过期元素时继续。

线程池知识点

SpringBoot中CommandLineRunner和ApplicationRunner接口解析和使用

2.步骤

2.1 创建需要延迟处理的类实现接口Delayed,重写getDelay和compareTo方法,如延迟订单类

import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class OrderDelay implements Delayed{
	
	private Long orderId;
    private Long createTime; // 创建时间
    
    public OrderDelay(Long orderId,Long createTime){
        this.orderId = orderId;
        this.createTime =  createTime;
    }

    // 判断对象是否过期(小于等于0时过期)
    @Override
    public long getDelay(TimeUnit unit) {
    	return createTime+24*60*60*1000l-System.currentTimeMillis(); // 创建订单24小时未支付即过期
    }
    
    // 根据对象过期时间排序, 哪个元素最早到过期时间, 就排在前面 
	@Override
	public int compareTo(Delayed delayed) {
		OrderDelay that = (OrderDelay)delayed;
        if(this.createTime>that.createTime){
            return 1;
        }else if(this.createTime==that.createTime){
            return 0;
        }else {
            return -1;
        }
	}

	public Long getOrderId() {
		return orderId;
	}

	public Long getCreateTime() {
		return createTime;
	}
	
}

2.2 定义一个Job类,实现CommandLineRunner接口,用于在项目启动后执行该任务,通过take()方法从队列中获取到指定对象出列

@Component
public class OrderDelayQueueJob implements CommandLineRunner {
	
    // 创建一个延迟队列
	public static DelayQueue<OrderDelay> delayQueue = new DelayQueue();
	
	@Autowired
	OrderMapper orderMapper;
	
	@Autowired
	AliPayService aliPayService;
	
	@Autowired
	VxPayService vxPayService;
	
	// 超过24小时未支付订单自动关闭
	private void orderTask() {
		List<Order> payingOrders = orderMapper.listPayingOrder();
		for(int i=0; i<payingOrders.size(); i++) {
			Order order = payingOrders.get(i);
			OrderDelay orderDelay = new OrderDelay(order.getId(), order.getCreateTime().toInstant().toEpochMilli());
			OrderDelayQueueJob.delayQueue.add(orderDelay);
		}
		
        while (true) {
            try {
            	OrderDelay orderDelay = OrderDelayQueueJob.delayQueue.take();
            	Integer orderStatus = orderMapper.getOrderStatus(orderDelay.getOrderId());
            	if(orderStatus == 0) {
            		orderMapper.cancelOrder(orderDelay.getOrderId());
            		Order order = orderMapper.getOrderById(orderDelay.getOrderId());
            		if(order.getPayType()==1) { // 支付宝订单
            			try {
							aliPayService.closePay(order.getId());
						} catch (AlipayApiException e) {
							e.printStackTrace();
						}
            		}else if(order.getPayType()==2) { // 微信订单
            			try {
							vxPayService.closeOrder(order.getId());
						} catch (Exception e) {
							e.printStackTrace();
						}
            		}else {
            			;
            		}
            		LogUtil.info("待支付订单队列剩余:"+OrderDelayQueueJob.delayQueue.size()+",订单关闭:"+orderDelay.getOrderId()+",订单创建时间:"+LocalDateTime.ofInstant(Instant.ofEpochMilli(orderDelay.getCreateTime()), ZoneId.of("GMT+8")));
            	}
            } catch (InterruptedException e) {
                break;
            }
        }
    }

	@Override
	public void run(String... args) throws Exception {
		// 自动取消订单开启
        ThreadUtil.execute(this::orderTask);
	}

}

2.3 在创建订单时,将该订单放入延迟队列中

Order order = new Order();
order.setOrderNumber(String.valueOf(now.toInstant().toEpochMilli()));
order.setUserId(userId);
order.setSubjectName(productName);
order.setProductType(productType);
order.setPayAmount(payAmount);
order.setYears(years);
order.setOrderStatus(0);
order.setPayType(1);
order.setCreateTime(now);
order.setIsDelete(0);
orderMapper.addOrder(order);
OrderDelay orderDelay = new OrderDelay(order.getId(), order.getCreateTime().toInstant().toEpochMilli());
OrderDelayQueueJob.delayQueue.add(orderDelay); // 将延迟订单放入延迟队列中

2.4当客户取消订单的时候,从Queue里面删除订单信息

方案5:消息队列:mq

使用RabbitMq 实现 RabbitMq实现延迟队列

优点: 开源,现成的稳定的实现方案;

缺点: RabbitMq是一个消息中间件;延迟队列只是其中一个小功能,如果团队技术栈中本来就是使用RabbitMq那还好,如果不是,那为了使用延迟队列而去部署一套RabbitMq成本有点大;

思考归纳:

1.延迟处理方案

1.延迟队列能做什么?
在我们的业务中通常会有一些需求是这样的:
	1. 淘宝订单业务:下单之后如果三十分钟之内没有付款就自动取消订单。
	2. 饿了吗订餐通知:下单成功后60s之后给用户发送短信通知。

那么这类业务我们可以总结出一个特点:需要延迟工作。
由此的情况,就是我们的DelayQueue应用需求的产生。

2.如何实现延迟队列

3.springboot如何整合延迟队列

4.性能如何优化?如何做到高可靠、高可用?

对于订单超时场景处理的思考(单机):https://zhuanlan.zhihu.com/p/99406453
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值