手把手带你实现一套延时队列(队列专题)

292 阅读4分钟

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

写在前面

队列一直是并发套餐中不可或缺的一部分,在专题的第一篇文章中我们已经讲过。今天我们将继续学习队列中比较重要的一个队列----延时队列。

欢迎大家点击头像查看专题,设计模式专题已完结,目前正在进行的是队列专题和Security专题。

延时队列使用场景

  • 缓存系统设计:使用DelayQueue保存缓存元素的有效期,用一个线程循环查询DelayQueue,一旦从DelayQueue中取出元素,就表示有元素到期。

  • 定时任务调度:使用DelayQueue保存当天要执行的任务和执行的时间,一旦从DelayQueue中获取到任务,就开始执行,比如Timer,就是基于DelayQueue实现的。

回顾一下

阻塞队列可分为以下几种:

image.png

今天我们就是要讲的就是DelayQueue

阻塞队列(Blocking Queue)提供了可阻塞的 put 和 take 方法,它们与可定时的 offer 和 poll 是等价的。如果队列满了 put 方法会被阻塞等到有空间可用再将元素插入;如果队列是空的,那么take 方法也会阻塞,直到有元素可用。当队列永远不会被充满时,put 方法和 take 方法就永远不会阻塞。

讲延时队列之前,我们先看源码 再写实例,循序渐进。

看源码,写实例

DelayQueue——延时队列,提供了在指定时间才能获取队列元素的功能。也就是说只有在队列加入元素后指定时间间隔后才能取出元素。

image.png

image.png

从源码中我们可以了解到:

  • 存放DelayQueue的元素,必须继承Delay接口,Delay接口使对象成为延迟对象。

  • 该接口强制实现两个方法:

    1.CompareTo(Delayed o):用于比较延时,队列里元素的排序依据,这个是Comparable接口的方法,因为Delay实现了Comparable接口,所以需要实现。

    2.getDelay(TimeUnit unit):这个接口返回到激活日期的--剩余时间,时间单位由单位参数指定。

  • 此队列不允许使用null元素。

我们接下来写个简单实例

代码应用

我们需要先实现DelayQueue,实现其getDelay 和 compareTo方法(继承了Comparable,用于延迟队列内部比较排序 当前时间的延迟时间,比较对象的延迟时间)。

/**
 * 声明一个延时任务队列
 * @Date 2021/11/14 5:30 下午
 * @Author yn
 */
@Data
public class TaskDelay implements Delayed {
    //任务ID
    private int taskId;
    //任务延时时间
    private Date taskTime;
    // 延时30秒
    private static final int EXPIRE_TIME = 30 * 1000;


    //设置延时时间
    @Override
    public long getDelay(TimeUnit unit) {
        return taskTime.getTime() + EXPIRE_TIME - System.currentTimeMillis();
    }

    //延迟排序
    @Override
    public int compareTo(Delayed o) {
        return this.taskTime.getTime() - ((TaskDelay) o).taskTime.getTime() > 0 ? 1 : -1;
    }
}

上面我们定义了一个队列,我们看下如何使用

public class TaskDelayApplication {

    //声明队列
    static DelayQueue<TaskDelay> queue = new DelayQueue<>();
    
    //检查任务
    private static void checkTask() {
            while (true) {
                try {
                    TaskDelay delay = queue.take();
                    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    System.out.println(formatter.format(new Date()) + ":任务被触发,任务id:" + delay.getTaskId());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    }
    //创建任务
    private static void createTask(int taskId) {
        TaskDelay delay = new TaskDelay();
        delay.setTaskId(taskId);
        Date currentTime = new Date();
        delay.setTaskTime(currentTime);
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(formatter.format(currentTime) + ":任务被创建,任务id:" + taskId);
        queue.put(delay);
    }
    
    //模拟调用
    public static void main(String[] args) throws InterruptedException {
        Thread createTaskThread = new Thread(() -> {
            
            //循环五个任务
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                createTask(i);
            }
        });
        //启动创建线程
        createTaskThread.start();
        
        //开始调用
        Thread checkTaskThread = new Thread(() -> {
            checkTask();
        });
        //启动检查线程
        checkTaskThread.start();
    }
}

我们看下调用结果。是不是如我们所想那样。我们上面默认设置了 30秒。

2021-11-14 19:12:58:任务被创建,任务id:1
2021-11-14 19:12:59:任务被创建,任务id:2
2021-11-14 19:13:00:任务被创建,任务id:3
2021-11-14 19:13:02:任务被创建,任务id:4


2021-11-14 19:13:28:任务被触发,任务id:1
2021-11-14 19:13:29:任务被触发,任务id:2
2021-11-14 19:13:30:任务被触发,任务id:3
2021-11-14 19:13:32:任务被触发,任务id:4

可以看到,30秒刚好 从队列里取出来数据

OK,今天到延时队列先讲到这里,后续会持续更新队列专题。RocketMQ等mq系列也会后续更新

总结

还有一些其它方法也可以实现延时队列,比如使用 redis 的 sortedset ,还有一些比较复杂的延时队列的算法实现的,比如: 时间轮 。Kafka、Netty 都有基于时间轮算法实现延时队列。这些就不再介绍,后续会做出相应的学习,我会再给大家写出来

欢迎大家点击头像关注我的并发队列专题,后续我们一起学习

弦外之音

感谢你的阅读,如果你感觉学到了东西,您可以点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作