关于springboot:订单自动过期实现方案

53次阅读

共计 4857 个字符,预计需要花费 13 分钟才能阅读完成。

需要剖析:

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();}

后果和下面的一样,图就不截了,置信我。

过期揭示

基于 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