订单自动过期时实现方案

具体是因为要做的项目需要做一个过滤过期任务的方案:一开始考虑做定时任务,但发现定时任务并不能完成要求,因为定时任务只能是静态某一时间进行遍历数据库进行修改操作,一般用于清楚数据库缓存等,所以考虑做延时队列

1. 延时队列

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.qingyuan.pigeon;

import lombok.Data;
import org.jetbrains.annotations.NotNull;
import java.util.Date;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
* @author han long yi
* @create 2020-11-20 16:14
*/
@Data
public class OrderDelayDto implements Delayed {
/**
* 任务编号
*/
private String taskId;

/**
* 过期时间
*/
private Date taskEndTime;

/**
* 判断过期条件:过期时间大于等于当前时间就算过期
* @param unit
* @return
* getDelay: 返回与此对象关联的剩余延迟,以给定的时间单位表示。
*/
@Override
public long getDelay(@NotNull TimeUnit unit) {
return unit.convert(this.taskEndTime.getTime() - System.currentTimeMillis(), TimeUnit.NANOSECONDS);
}

/**
* 订单加入队列的排序规则:最先过期的排在前面
* @param o
* @return
* compareTo: 将此对象与order的指定对象进行比较。当此对象小于、等于或大于指定对象时,返回负整数、零或正整数。
*/
@Override
public int compareTo(@NotNull Delayed o) {
OrderDelayDto orderDelayDto = (OrderDelayDto) o;
/**
* Date.getTime(long):
* 返回此{@code Date}对象表示的自1970年1月1日00:00:00 GMT*以来的毫秒数。
*/
long time =orderDelayDto.getTaskEndTime().getTime();
long time1 = this.taskEndTime.getTime();
/**
* 如果 time == time1 返回0
* 如果 time < time1 返回1
* 如果 time > time1 返回-1
*/
return time == time1 ? 0 : time < time1 ? 1 : -1;
}
}

  • convert : 将给定单位中的给定持续时间转换为该单位。
  • System.currentTimeMillis() : 返回当前时间(毫秒)。
  • NANOSECONDS :表示千分之一微秒的时间单位。

写个test 方法测试一下,创建两个订单o1和o2,放入到延时队列中,然后while()方法不断的去取。
在此方法内通过队列的take()方法获得已过期的订单,然后做出相应的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Test
void teat(){
DelayQueue<OrderDelayDto> queue = new DelayQueue<>();

/**
* 第一个订单,过期时间设置为一分钟后
*
*/
OrderDelayDto o1 = new OrderDelayDto();
o1.setTaskId("1001");
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE,1);
o1.setTaskEndTime(calendar.getTime());

/**
* 第二个订单,过期时间设置为现在
*/
OrderDelayDto o2 = new OrderDelayDto();
o2.setTaskId("1002");
o2.setTaskEndTime(new Date());

/**
* 往队列中放入数据
*/
queue.offer(o1);
queue.offer(o2);

/**
* 延时队列
*/
while (true) {
try {
OrderDelayDto take = queue.take();
System.out.println("订单编号:" + take.getTaskId() + "过期时间:" + take.getTaskEndTime());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

测试结果

QQ图片20201120172903.png

即便往队列中放入数据时,先放入o1,先取出的依旧是o2,以此验证队列的排序规则是谁最先过期,无关放入队列的顺序!

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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();
}
}
}
}
1234567891011121314151617181920212223242526

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();
}