专栏首页咸鱼不闲订单自动过期实现方案

订单自动过期实现方案

需求分析:

24小时内未支付的订单过期失效。

解决方案

  1. 被动设置:在查询订单的时候检查是否过期并设置过期状态。
  2. 定时调度:定时器定时查询并过期需要过期的订单。
  3. 延时队列:将未支付的订单放入一个延时队列中,依次取出过期订单。
  4. 过期提醒:reids支持将一个过期的key(订单号)通知给客户端,根据过期的订单号进行相应的处理。

1. 被动设置

这个太简单了,就是在查询的时候判断是否失效,如果失效了就给他设置失效状态。但是弊端也很明显,每次查询都要对未失效的订单做判断,如果用户不查询,订单就不失效,那么如果有类似统计失效状态个数的功能,将会受到影响,所以只能适用于简单独立的场景。简直low爆了。

2. 定时调度

这种是常见的方法,利用一个定时器,在设置的周期内轮询检查并处理需要过期的订单。 具体实现有基于Timer的,有基于Quartz,还有springboot自带的Scheduler,实现起来比较简单。 就写一下第三个的实现方法吧:

  1. 启动类加上注解@EnableScheduling
  2. 新建一个定时调度类,方法上加上@Scheduled注解,如下图那么简单。

弊端

  1. 不能够精准的去处理过期订单,轮询周期设置的越小,精准度越高,但是项目的压力越大,我们上一个项目就有这种状况,太多定时器在跑,项目运行起来比较笨重。
  2. 而且需要处理的是过期的订单,但是要查询所有未支付的订单,范围大。对于大订单量的操作不合适。

3. 延时队列

基于JDK的实现方法,将未支付的订单放到一个有序的队列中,程序会自动依次取出过期的订单。 如果当前没有过期的订单,就会阻塞,直至有过期的订单。由于每次只处理过期的订单,并且处理的时间也很精准,不存在定时调度方案的那两个弊端。 实现: 1.首先创建一个订单类OrderDelayDto需要实现Delayed接口。然后重写getDelay()方法和compareTo()方法,只加了订单编号和过期时间两个属性。 这两个方法很重要, getDelay()方法实现过期的策略,比如,订单的过期时间等于当前时间就是过期,返回负数就代表需要处理。否则不处理。 compareTo()方法实现订单在队列中的排序规则,这样即使后面加入的订单,也能加入到排序中,我这里写的规则是按照过期时间排序,最先过期的排到最前面,这一点很重要,因为排在最前面的如果没有被处理,就会进入阻塞状态,后面的不会被处理。

import lombok.Data;
import java.util.Date;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * @author mashu
 * Date 2020/5/17 16:25
 */
@Data
public class OrderDelayDto implements Delayed {
    /**
     * 订单编号
     */
    private String orderCode;
    /**
     * 过期时间
     */
    private Date expirationTime;

    /**
     * 判断过期的策略:过期时间大于等于当前时间就算过期
     *
     * @param unit
     * @return
     */
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(this.expirationTime.getTime() - System.currentTimeMillis(), TimeUnit.NANOSECONDS);
    }

    /**
     * 订单加入队列的排序规则
     *
     * @param o
     * @return
     */
    @Override
    public int compareTo(Delayed o) {
        OrderDelayDto orderDelayDto = (OrderDelayDto) o;
        long time = orderDelayDto.getExpirationTime().getTime();
        long time1 = this.getExpirationTime().getTime();
        return time == time1 ? 0 : time < time1 ? 1 : -1;
    }
}

其实这样已经算是写好了。我没有耍你。 写个main 方法测试一下,创建两个订单o1和o2,放入到延时队列中,然后while()方法不断的去取。 在此方法内通过队列的take()方法获得已过期的订单,然后做出相应的处理。

 public static void main(String[] args) {
        DelayQueue<OrderDelayDto> queue = new DelayQueue<>();
        OrderDelayDto o1 = new OrderDelayDto();
        //第一个订单,过期时间设置为一分钟后
        o1.setOrderCode("1001");
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, 1);
        o1.setExpirationTime(calendar.getTime());
        OrderDelayDto o2 = new OrderDelayDto();
        //第二个订单,过期时间设置为现在
        o2.setOrderCode("1002");
        o2.setExpirationTime(new Date());
        //往队列中放入数据
        queue.offer(o1);
        queue.offer(o2);
        // 延时队列
        while (true) {
            try {
                OrderDelayDto take = queue.take();
                System.out.println("订单编号:" + take.getOrderCode() + " 过期时间:" + take.getExpirationTime());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

运行结果:

我故意把第二个订单的过期时间设置为第一个订单之前,从结果可以看出,他们已经自动排序把最先过期的排到了最前面。 第一个订单的失效时间是当前时间的后一分钟,结果也显示一分钟后处理了第一条订单。

2.然而通常情况下,我们会使用多线程去取延时队列中的数据,这样即使线程启动之后也能动态的向队列中添加订单。 创建一个线程类OrderCheckScheduler实现Runnable接口, 添加一个延时队列属性,重写run()方法,在此方法内通过队列的take()方法获得已过期的订单,然后做出相应的处理。

import java.util.concurrent.DelayQueue;
/**
 * @author mashu
 * Date 2020/5/17 14:27
 */
public class OrderCheckScheduler implements Runnable {

  // 延时队列
  private DelayQueue<OrderDelayDto> queue;

  public OrderCheckScheduler(DelayQueue<OrderDelayDto> queue) {
      this.queue = queue;
  }

  @Override
  public void run() {
      while (true) {
          try {
              OrderDelayDto take = queue.take();
              System.out.println("订单编号:" + take.getOrderCode() + " 过期时间:" + take.getExpirationTime());
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
  }
}

好了,写个方法测试一下:

    public static void main(String[] args) {
        // 创建延时队列
        DelayQueue<OrderDelayDto> queue = new DelayQueue<>();
        OrderDelayDto o1 = new OrderDelayDto();
        //第一个订单,过期时间设置为一分钟后
        o1.setOrderCode("1001");
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, 1);
        o1.setExpirationTime(calendar.getTime());
        OrderDelayDto o2 = new OrderDelayDto();
        //第二个订单,过期时间设置为现在
        o2.setOrderCode("1002");
        o2.setExpirationTime(new Date());
        //运行线程
        ExecutorService exec = Executors.newFixedThreadPool(1);
        exec.execute(new OrderCheckScheduler(queue));
        //往队列中放入数据
        queue.offer(o1);
        queue.offer(o2);
        exec.shutdown();
    }

结果和上面的一样,图就不截了,相信我。

4. 过期提醒

基于redis的过期提醒功能,听名字就知道这个方案最是纯真、最直接的,就是单纯处理过期的订单。 修改个redis的配置吧先,因为redis默认不开启过期提醒。 notify-keyspace-events改为notify-keyspace-events "Ex" 写一个类用来接收来自redis的暖心提醒OrderExpirationListener,继承一下KeyExpirationEventMessageListener抽象类。重写onMessage()方法,在此方法中处理接收到的过期key.

import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
import java.util.Date;

/**
 * @author mashu
 * Date 2020/5/17 23:01
 */
@Component
public class OrderExpirationListener extends KeyExpirationEventMessageListener {

    public OrderExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        final String expiredKey = message.toString();
        System.out.println("我过期了" + expiredKey+"当前时间:"+new Date());
    }
}

ok,向redis中存入一个订单,过期时间为1分钟。

redis.set("orderCode/10010", "1", 1L, TimeUnit.MINUTES);
System.out.println("redis存入订单号 key: orderCode/10010,value:1,过期时间一分钟,当前时间"+new Date());

运行结果:

除此之外还有用到消息队列的。夜深了,我得玩会游戏了。

没有绝对的好方案,只有在不同场景下的更合适的方案。随着需求的变化,技术的革新,方案也会不断的被优化和迭代,唯一不变的是工资。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • jsoup爬虫工具的简单使用

    解决方案: 1.通过url 获得doucment对象, 2.调用select()等方法获得Elements对象, 3.调用.text()等方法,获得自己想要的内...

    Mshu
  • String的基本用法

    Mshu
  • 记一次还可以抢救一下的爬虫私活,求接盘!

    最近接了一个爬虫的私活,收益颇丰。自认为对爬虫掌握的还算不错,爬过很多国内外网站, 数据超过百万,应对过封IP、设验证码、假数据、强制登录等反爬虫手段。于是乎,...

    Mshu
  • 微信公众号迁移Serverless详解

    3月腾讯云函数计算开放测试, 看到的第一反应是这种Serverless太适合做微信公众号的后端来实现自动应答了。

    朱小一
  • Redis 过期时间与内存管理

    当 Redis 作为缓存使用时(此时缓存仅作为热点数据提高服务的访问性能),需要考虑内存的限制,以及如何随着业务的增长,仅保留热点数据。

    斯武丶风晴
  • 开源CEGUI编辑器之一(MFC重写的LayoutEditor)

    转载请注明出处:帘卷西风的专栏(http://blog.csdn.net/ljxfblog)

    帘卷西风
  • 移动端网页怎么做?

    移动端网页最大的特点是什么?自适应不同尺寸的屏幕!高大上的叫法:响应式! 知道了自适应网页怎么做岂不是能很好解决问题了?那么自适应网页怎么做呢?网上关于这方面的...

    前端博客 : alili.tech
  • [Tensorflow] 使用SSD-MobileNet训练模型

    因为Android Demo里的模型是已经训练好的,模型保存的label都是固定的,所以我们在使用的时候会发现还有很多东西它识别不出来。那么我们就需要用它来训练...

    wOw
  • [原创源码] 京东助手各版本开源

    PS:事先说明,我只是个小白,大佬们都别喷,这是第一次用易语言写一个软件写这么久! 先把源码公开一下,请大家来一波吾币! 里面包含着最近写的每个版本的源码,...

    凯哥Java
  • JS中的事件循环机制与宏队列、微队列笔记

    为什么JavaScript是一门单线程语言?作为一门浏览器脚本语言,它的主要用途就是操作DOM和与用户交互设计,如果说js是多线程的话,那么它在操作DOM的时候...

    帅的一麻皮

扫码关注云+社区

领取腾讯云代金券