一、什么是延迟任务
什么是延迟任务?
12306下单等待支付业务:
定时任务与延迟任务的区别:
定时任务往往是固定周期的,有明确的触发时间。而延迟任务一般没有固定的开始时间,它常常是由一个事件触发的,而在这个事件触发之后的一段时间内触发另一个事件,任务可以立即执行,也可以延迟,任务之间也可以建立一定联系;
场景一: 订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单;如果期间下单成功,任务取消
场景二:接口对接出现网络问题,1分钟后重试,如果失败,2分钟重试,直到出现阈值终止
延迟任务的实现方案:
1:单机版方案:基于jdk中的定时器
2:消息中间件方案
3:自定义分布式延迟任务方案
方案没有好坏之分,和系统架构一样,关键是适合自身业务系统,不过目前IT行业发展阶段,有些市面上开源中间件已经不能满足大厂自身业务,大厂基本都是从新定制开发。
二、延迟任务系统设计
2.1.延迟任务需求分析
延迟任务的概念我们已经阐述,在充吧项目中延迟任务的业务场景有:
场景一: 订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单;如果期间下单成功,任务取消
场景二:接口对接出现网络问题,1分钟后重试,如果失败,2分钟重试,直到出现阈值终止
总结下来:简单的理解延迟任务可以认为是在某个设定的时间之后做某一件事情。
2.2.延迟任务单机版实现方案-Timer定时器
jdk中提供了一个定时器类Timer,它能做到在指定的时间点去执行某一个任务,也能做到定时的周期性的执行某一个任务
1:在src/test/java下创建测试包:com.chongba.schedule.jdk,在测试包下创建测试类TimerTaskTest
public class TimerTaskTest { public static void main(String[] args) { Timer timer = new Timer(); // 1秒之后 执行任务 timer.schedule(new TimerTask() { @Override public void run() { System.out.println(System.currentTimeMillis()/1000+"执行了任务"); } },1000L); System.out.println(System.currentTimeMillis()/1000); //执行时间 <=当前时间 则任务立马执行 timer.schedule(new TimerTask() { @Override public void run() { System.out.println(System.currentTimeMillis()/1000+"执行了任务"); } },new Date(System.currentTimeMillis()-1000L)); System.out.println(System.currentTimeMillis()/1000); // 延迟1秒之后执行任务,然后每隔2秒执行任务 timer.schedule(new TimerTask() { @Override public void run() { System.out.println(System.currentTimeMillis()/1000+"执行了任务"); } },1000L,2000L); System.out.println(System.currentTimeMillis()/1000); // 执行时间 <=当前时间 则任务立马执行 然后每隔2秒执行任务 timer.schedule(new TimerTask() { @Override public void run() { System.out.println(System.currentTimeMillis()/1000+"执行了任务"); } },new Date(System.currentTimeMillis()- 1000L),2000L); System.out.println(System.currentTimeMillis()/1000); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println(System.currentTimeMillis()/1000+"执行了任务"); } },3000L,1000L); System.out.println(System.currentTimeMillis()/1000); //执行时间 <=当前时间 任务立马执行 并会计算过期该执行的次数并且执行,最后每隔1秒执行一次 timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println(System.currentTimeMillis()/1000+"执行了任务"); } },new Date(System.currentTimeMillis() - 3000L),1000L); System.out.println(System.currentTimeMillis()/1000); } } class TWO{ public static void main(String[] args) { Timer timer = new Timer(); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { try { System.out.println(System.currentTimeMillis()/1000+"执行了任务"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } },0L,2000L); timer.schedule(new TimerTask() { @Override public void run() { try { System.out.println(System.currentTimeMillis()/1000+"执行了任务"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } },0,2000L); } }
1.schedule() ,2个参数方法: 在执行任务时,如果指定的计划执行时间scheduledExecutionTime <= systemCurrentTime,则task会被立即执行。
2.schedule() ,3个参数方法: 在执行任务时,如果指定的计划执行时间scheduledExecutionTime <= systemCurrentTime,则task会被立即执行,之后按period参数固定重复执行。
3.scheduleAtFixedRate() ,3个参数方法: 在执行任务时,如果指定的计划执行时间scheduledExecutionTime<= systemCurrentTime,则task会首先按执行一次;然后按照执行时间、系统当前时间和period参数计算出过期该执行的次数,计算按照: (systemCurrentTime-scheduledExecutionTime)/period,再次执行计算出的次数;最后按period参数固定重复执行。
4.schedule() 和scheduleAtFixedRate() schedule()方法更注重保持间隔时间的稳定。 scheduleAtFixedRate()方法更注重保持执行频率的稳定。
注意,阿里推荐的编码规约中这样写道:
Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行
Timer 优化方案:
使用ScheduledExecutorService则没有这个问题。
测试:
1:在测试包jdk下创建测试类TimerTaskTest2
public class TimerTaskTest2 { public static void main(String[] args) { Timer timer = new Timer(); for(int i=0;i<100;i++){ int finalI = i; timer.schedule(new TimerTask() { @Override public void run() { System.out.println(finalI); if(finalI == 20){ throw new RuntimeException(); } } },1000L); } ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5); for(int i=0;i<100;i++){ int finalI = i; scheduledExecutorService.schedule(new TimerTask() { @Override public void run() { System.out.println(finalI); if( finalI ==20 ){ throw new RuntimeException(); } } },1, TimeUnit.SECONDS); } } }
另外对于ScheduledExecutorService还有其他的API
scheduledExecutorService.scheduleAtFixedRate() // 按照固定频率 scheduledExecutorService.scheduleWithFixedDelay()// 如果任务时间超过固定频率,按照任务实际时间延后
2.3.延迟任务单机版实现方案-DelayQueue
DelayQueue 是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。
DelayQueue属于排序队列,它的特殊之处在于队列的元素必须实现Delayed接口,该接口需要实现compareTo和getDelay方法
getDelay方法:获取元素在队列中的剩余时间,只有当剩余时间为0时元素才可以出队列。
compareTo方法:用于排序,确定元素出队列的顺序。
实现:
1:在测试包jdk下创建延迟任务元素对象DelayedTask,实现compareTo和getDelay方法,
2:在main方法中创建DelayQueue并向延迟队列中添加三个延迟任务,
3:循环的从延迟队列中拉取任务
public class DelayedTask implements Delayed{ // 任务的执行时间 private int executeTime = 0; public DelayedTask(int delay){ Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.SECOND,delay); this.executeTime = (int)(calendar.getTimeInMillis() /1000 ); } /** * 元素在队列中的剩余时间 * @param unit * @return */ @Override public long getDelay(TimeUnit unit) { Calendar calendar = Calendar.getInstance(); return executeTime - (calendar.getTimeInMillis()/1000); } /** * 元素排序 * @param o * @return */ @Override public int compareTo(Delayed o) { long val = this.getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS); return val == 0 ? 0 : ( val < 0 ? -1: 1 ); } public static void main(String[] args) { DelayQueue<DelayedTask> queue = new DelayQueue<DelayedTask>(); queue.add(new DelayedTask(5)); queue.add(new DelayedTask(10)); queue.add(new DelayedTask(15)); System.out.println(System.currentTimeMillis()/1000+" start consume "); while(queue.size() != 0){ DelayedTask delayedTask = queue.poll(); if(delayedTask !=null ){ System.out.println(System.currentTimeMillis()/1000+" cosume task"); } //每隔一秒消费一次 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }