一、什么是延迟任务

在开发中,往往会遇到一些关于延时任务的需求。例如

  • 生成订单30分钟未支付,则自动取消
  • 生成订单60秒后,给用户发短信

对上述的任务,我们给一个专业的名字来形容,那就是延时任务。延时任务属于定时任务的一种,不同于一般的定时任务,延时任务是在某事件触发后的未来某个时刻执行,没有重复的执行周期。

1-延时任务.png

应用场景:

场景一:订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单;如果期间下单成功,任务取消

场景二:接口对接出现网络问题,1分钟后重试,如果失败,2分钟重试,直到出现阈值终止

二、技术对比

1、DelayQueue

JDK自带DelayQueue 是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素

2-延时任务.png

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;

    private int delay=0;
    
    public DelayedTask(int delay){
        this.delay=delay;
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND,delay);
        this.executeTime = (int)(calendar.getTimeInMillis() /1000 );
    }

    public String getTaskName(){
        return "剩余"+delay+"的任务";
    }

    /**
     * 元素在队列中的剩余时间
     * @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(delayedTask.getTaskName()+",任务时间已到"+System.currentTimeMillis()/1000+" cosume task");
            }
            //每隔一秒消费一次
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }     
    }
}

使用DelayQueue的问题:

(1)都在内存中运行,服务器重启后,数据全部消失

(2)集群扩展相当麻烦   

(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常   
(4)代码复杂度较高

2、数据库轮询

  • 将任务存到数据库,然后用定时器轮询
  • 问题:
    • 小型系统如果只有几万任务,采用上述方案即可,如果稍大规模系统,任务量过大,对数据库造成的压力过大

3、数据库+redis实现(推荐)

zset数据类型的去重有序(分数排序)特点进行延迟。例如:时间戳作为score进行排序

3-延时任务.png

三、Redis实现延迟队列

  • 实现思路

4-延时任务.png

  • 问题思路

1.为什么任务需要存储在数据库中?

​ 延迟任务是一个通用的服务,任何需要延迟得任务都可以调用该服务,需要考虑数据持久化的问题,存储数据库中是一种数据安全的考虑(备份机制)。

2.为什么redis中使用两种数据类型,list和zset?

​ 效率问题,算法的时间复杂度

3 zset还做了那些优化

​ 不同任务不同Key,进一步优化