对于商城系统来说,一般都有订单到期未支付取消订单的操作,我们规模较小目前没有引入消息中间件。这个功能之前是通过监听Redis的key过期事件来实现的,后续了解到Reids这种方案受限于Redis的过期策略,如果这个key过期未被Redis扫描到,那么就不会触发过期事件订单也不会关闭。
如果单纯采用定时任务的方式,定时扫表不仅会增加数据库压力,订单关闭的实时性也无法保证。
想起来JDK本身提供了延时队列的实现,缺陷是这个数据是保存在内存中,服务重启或宕机都会丢失数据,针对这一点,其实可以放入延时队列之前就将数据入库,表的字段可以自己根据需要设计,大概需要三个字段
- 订单Id
- 到期的时间戳
- 以及是否被处理
那么重启服务时,就可以监听容器的启动事件,将数据恢复到内存中,然后启动线程消费延时队列,当消费一个订单后,将是否被处理的字段打上标识,防止下次被服务重启被重复处理。
JDK提供的延时队列是java.util.concurrent.DelayQueue
,改队列中的元素必须实现java.util.concurrent.Delayed
接口,下面是实现的案例
public class Order implements Delayed {
private final String orderId;
private final Long expireTime;
public Order(Long delay, TimeUnit unit, String orderId) {
this.orderId = orderId;
this.expireTime = System.currentTimeMillis() + unit.toMillis(delay);
}
@Override
public String toString() {
return new StringJoiner(", ", Order.class.getSimpleName() + "[", "]")
.add("orderId='" + orderId + "'")
.add("expireTime=" + expireTime)
.toString();
}
@Override
public long getDelay(TimeUnit unit) {
System.out.println("到期了么?");
long l = expireTime - System.currentTimeMillis();
return l;
}
@Override
public int compareTo(Delayed o) {
long l = this.expireTime - ((Order) o).expireTime;
return (int) l;
}
/**
* 测试案例
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
DelayQueue<Order> orders = new DelayQueue<>();
orders.put(new Order(2L, TimeUnit.SECONDS, "2秒过期"));
orders.put(new Order(5L, TimeUnit.SECONDS, "5秒过期"));
for (int i = 0; i < 2; i++) {
Order take = orders.take();
System.out.println(take);
}
}
}
需要注意的是DelayQueue中的元素是通过PriorityQueue来存储队列中的元素的,这个队列是无界队列,如果订单量很大的话,会有OOM的风险,我们系统体量较小,所以采用这种方式问题不会太大
Order类需要实现两个方法:分别是compareTo()
和getDelay()
,上面的代码总体上和网上的实现思路相同,测试发现控制台中会不断打印getDelay()方法中输出的“到期了么?”,这明显不符合逻辑,DelayQueue这个队列是实现了BlockingQueue
接口,调用该队列的take()方法是应该会阻塞,而不是反复调用来判断是否到达延迟时间,问题就出在getDelay()
方法的参数上,通过debug发现,调用该方法时,TimeUnit传来的是NANOSECONDS这个纳秒单位,源码中是这么写的:
原来这里delay方法的返回值决定了线程的休眠时间,由于我们忽略了单位换算,导致这个休眠时间是以毫秒计算的,而这里休眠的方法则是按照纳秒处理,导致这个方法被反复调用了。
知道了原因,处理起来就比较简单了,将getDelay()
方法改成下面的写法,进行一次单位换算即可,这样返回的就是毫秒单位,那么上面休眠就是按照正确的数值进行处理的。
@Override
public long getDelay(TimeUnit unit) {
System.out.println("到期了么?");
long l = expireTime - System.currentTimeMillis();
return unit.convert(l, TimeUnit.MILLISECONDS);
}
对于这一点需要格外注意,否则线程并不是阻塞状态,从而浪费CPU资源。