基于Java DelayedQueue+Redis 实现订单过期关闭

571 阅读4分钟

前言

最近空闲时在写餐饮小程序,也是为了从头开始学习下,基于现实点餐的业务逻辑来摸一下其中的相关问题。 刚好写到用户下单的步骤逻辑,简单研究了下订单关闭,并实现了Java原生的延迟队列DelayedQueue+Redis实现订单关闭的逻辑。

关于过期订单自动关闭

在网上搜索订单过期关闭的方法,有很多文章,在此不再赘述。 可点此处查看其中一篇
简单来说,需要在某用户订单过期时主动生产(推送消息),服务端接收到消息进行消费(需要关心消费的正确性)。
使用实际的技术选型,具体需要结合用户量和实际架构进行设计。

DelayedQueue简述

简单看下DelayedQueue的源码,可看出该类是Java集合框架的成员(This class is a member of the Java Collections Framework.)。

它实现了BlockingQueue<E>接口,拥有阻塞队列的特性(线程安全)BlockingQueue介绍

DelayedQueue中的元素需继承Delayed类(DelayQueue<E extends Delayed>),然而Delayed继承自Comparable,为使用该队列时比较元素中的延迟时间并获取最先到期的元素打下基础(需要重写getDelay、 compareTo两个方法)

image.png

案例代码

创建一个DelayedTask延迟任务类实现Delayed接口并重写方法,DelayedTask后续用于进入队列中。

@Data
public class DelayedTask<T> implements Delayed {

    private T data;

    // 此处有歧义,应为createTime开始放入队列的时间
    private long startTime;
    
    // 单位毫秒
    private long delayTime;

    // 判断 延长时间 - 当前时间和 元素.开始放入队列时间 的区别
    @Override
    public long getDelay(@NotNull TimeUnit unit) {
        long diff = delayTime - (System.currentTimeMillis() - startTime);
        return unit.convert(diff, TimeUnit.MILLISECONDS);
    }
    
    // 与其他元素进行对比,getDelay对比后其他元素先出队列
    @Override
    public int compareTo(@NotNull Delayed o) {
        if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS)) {
            return -1;
        }
        if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) {
            return 1;
        }
        return 0;
    }
}

定义一个订单处理器,和订单过期处理队列,进行订单过期处理。

@Component
@Slf4j
@RequiredArgsConstructor
public class UserPayOrderQueueHandler {

    private final RedisUtils redisUtils;

    private final DelayQueue<DelayedTask<BizPayOrder>> payOrderQueue = new DelayQueue<>();

    private final IBizPayOrderService payOrderService;

    public static final String NON_PAYMENT_ORDER_KEY_PREFIX = "NON-PAYMENT-ORDER:";

    private final BusinessConfig businessConfig;

    // 项目运行时,向线程池提交检查订单过期
    // 配置化,假如yml中配置使用DelayedQueue队列检查则开启一个内部不断运行的检查方法线程
    @PostConstruct
    public void handleOrderExpire() {
        if (HandleOrderExpireType.DELAYED_QUEUE.getName().equals(businessConfig.getCheckType())) {
            HutuThreadPoolExecutor.threadPool.submit(this::doOrderExpire);
            log.info("向线程池提交检查订单过期线程");
        }
    }

    // 订单过期检查方法
    public void doOrderExpire() {
        while (true) {
            try {
                // DelayedQueue.take()方法没有过期元素时(getDelayed != -1)时阻塞
                DelayedTask<BizPayOrder> delayedTask = payOrderQueue.take();
                // 获取先前DelayedTask<T>中T实际声明的类
                BizPayOrder data = delayedTask.getData();
                log.info("开始处理过期订单信息,订单id:[{}]", data.getId());
                Long userId = data.getUserId();
                Object object = redisUtils.hget(NON_PAYMENT_ORDER_KEY_PREFIX + userId, data.getId() + "");
                // 此处业务逻辑,省略...
                // 实际上Redis的作用是存储当时初始化的订单
                // 订单支付成功时需要删除当前用户下的未支付订单列表
                if (HutuUtils.isEmpty(object)) {
                    log.info("订单已完成,无需处理");
                    return;
                }
                LambdaUpdateWrapper<BizPayOrder> wrapper = new LambdaUpdateWrapper<>();
                wrapper.eq(BizPayOrder::getId, data.getId())
                        .set(BizPayOrder::getOrderStatus, OrderStatusEnum.EXPIRE.getVal());
                payOrderService.update(wrapper);
                redisUtils.hdel(NON_PAYMENT_ORDER_KEY_PREFIX + userId, data.getId());
                log.info("订单已超时,设置为过期状态...");
            } catch (InterruptedException e) {
                log.error(e.getLocalizedMessage(), e);
            }
        }
    }

    // 预留方法给具体订单业务放入订单过期延时检查任务类
    public void pushData2Queue(BizPayOrder data, long expireTime) {
        DelayedTask<BizPayOrder> settleOrderVoDelayedTask = new DelayedTask<>();
        settleOrderVoDelayedTask.setData(data);
        // 放入时间取订单创建时间
        settleOrderVoDelayedTask.setStartTime(data.getCreateTime().getTime());
        // 使用配置化的过期时长
        settleOrderVoDelayedTask.setDelayTime(TimeUnit.MILLISECONDS.convert(expireTime, TimeUnit.SECONDS));
        // 向队列中放入元素DelayedQueue.offer()
        // DelayedQueue是无底队列,offer和take都可以
        payOrderQueue.offer(settleOrderVoDelayedTask);
    }

}

生产订单数据,使用预留的pushData2Queue方法,在订单初始化(用户下单)正确处理完业务逻辑后时放入 此处省略...

因DelayedQueue是运行在jvm中,当项目停止时,内存信息消失,会丢失未处理的队列元素内容
作为补偿,我的项目启动时,主动查询一次当日未完成订单,并放入到监听队列里。

@Service
@Slf4j
public class BizPayOrderServiceImpl extends ServiceImpl<BizPayOrderMapper, BizPayOrder> implements IBizPayOrderService {
   
   ...

   @PostConstruct
   public void pushUnFinishOrder2Queue() {
       // 业务逻辑,省略
       List<BizPayOrder> unPayOrders = this.list(wrapper);
       if (HutuUtils.isNotEmpty(unPayOrders)) {
           unPayOrders.forEach(order -> {
               userPayOrderQueueHandler.pushData2Queue(order, businessConfig.getOrderExpire());
               redisUtils.hset(UserPayOrderQueueHandler.NON_PAYMENT_ORDER_KEY_PREFIX + order.getUserId(), order.getId() + "", JSONObject.toJSONString(order));
           });
           log.info("投入了{}个当日未完成订单到监听队列", unPayOrders.size());
       }
   }

实际运行

延迟时间设置为5秒,过期订单检查类型为延迟队列

image.png

下单,等待5秒

image.png

设置成30秒,重启,下单后马上重启应用

image.png

image.png

小结

此篇文章简单过了一遍DelayedQueue延迟队列实现订单过期的案例,篇幅和水平有限,具体的内容没有深入探讨,其中有很多东西笔者还在学习,希望能与读者共同进步,有更多的东西我会在此号下更新,感兴趣的朋友可以点点关注,谢谢。