简单实现延迟队列

昨天,人生的处女面,面试官问我如何实现延迟队列,然而不争气的我只知道用redis和ScheduledExecutorService来实现延迟队列,说白了,面试的时候,把延迟队列的实现方式想的太复杂了,总之还是我太菜了,菜是原罪啊啊啊啊,😫😫😫

认识过延迟队列吗?什么场景需要延迟队列?

问题出处:https://www.cnblogs.com/wangzhaobo/articles/9667636.html

什么是延迟队列?

延迟队列首先是个消息队列,其次是个带延迟功能的消息队列,你这么理解就对了。相对于普通消息队列,延迟队列中的消息除了消息本身外,还要有一个重要元素就是说明这条消息应该何时被消费掉!也就说在指定时间消费掉指定消息。

使用延迟队列我们可以解决什么问题?

  • 抢了一个小米手机,超过30分钟未支付,订单自动作废
  • 有些退款的订单,那么退款到账是如何检测的
  • 注册后到现在已经30天的用户,如何发短信撩动

这些业务场景就是典型的需要延迟队列的case。就比如说第一个案例,如果没有延迟队列怎么办?就是写脚本用定时器的方法去轮训,只能如此这样做,没有更好的办法。加入延迟队列后,这件事情就变得好玩了。

延迟队列的实现方式

使用Redis的zset结构来时实现

使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

优点: 简单实用,一针见血。

缺点:

  • 单个 zset 肯定支持不了太大的数据量,如果你有几百万的延迟任务需求,还是要换一个方案;
  • 定时器轮询方案可能会有异常终止的情况需要自己处理,同时消息处理失败的回滚方案,您也要自己处理。

所以,sorted set 的方案并不是一个成熟的方案,他只是一个快速可供落地的方案。

使用DelayQueue来实现延迟队列

参考:https://www.cnblogs.com/hhan/p/10678466.html

这个呢,是DelayQueue的主要部分。
在这里插入图片描述
我们通过上图我们可以知道,DelayQueue添加的元素,是需要实现Delayed类。
在这里插入图片描述
由Delayed定义可以得知,队列元素需要实现getDelay(TimeUnit unit)方法和compareTo(Delayed o)方法, getDelay定义了剩余到期时间,compareTo方法定义了元素排序规则,注意,元素的排序规则影响了元素的获取顺序,将在后面说明。

然后我们通过第一张图可以知道,DelayQueue存放元素的容器是由PriorityQueue类来实现的。

首先我们需要知道PriorityQueue类的数据结构是什么?
PriorityQueue(优先级队列)的数据结构是二叉堆。

二叉堆是一个特殊的堆, 它近似完全二叉树。
二叉堆满足特性: 父节点的键值总是保持固定的序关系于任何一个子节点的键值,且每个节点的左子树和右子树都是一个二叉堆。
当父节点的键值总是大于或等于任何一个子节点的键值时为最大堆。 当父节点的键值总是小于或等于任何一个子节点的键值时为最小堆。
下图是一个最大堆
在这里插入图片描述
然后再PriorityQueue类中有个方法就是:
在这里插入图片描述
然后我们再进一步的参看里面的那两个方法,我们就可以知道,PriorityQueue类是依靠,compare 或者 compareTo来进行排序的。

然后我们再来看看DelayQueue类的添加方法,我们可以知道其实DelayQueue是线程安全的,因为它的操作都需要先获取锁才能进行。
在这里插入图片描述
那么这个时候我们是否有一个疑惑,它是怎么实现延时的呢,当初面试官问我的时候,我以为,比如message要5分钟之后才发送,然后呢这个线程就要睡眠5分钟,比如调用wait方法,阻塞5分钟,然后再让其他线程把自己叫醒,然后我就没想出来就GG了,然后今天百度才知道可以用DelayQueue类来实现。

DelayQueue来实现延迟是这样的,里面用PriorityQueue来存放元素,并且呢,PriorityQueue维持一个最小堆,就是二叉堆的根节点是最小的。然后得到根节点,调用根节点对象存储的元素的getDelay方法,计算延迟时间,如果小于0,直接返回元素,如果大于0,那么就要有限等待,然后再返回。
在这里插入图片描述
代码如下:

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;


public class DelayedQueneTest {
    public static void main (String[] args) throws InterruptedException {
        DelayQueue<Item> queue = new DelayQueue();

        queue.add(new Item("大碗稀饭1",1,TimeUnit.SECONDS));
        queue.add(new Item("大碗稀饭2",3,TimeUnit.SECONDS));
        queue.add(new Item("大碗稀饭3",2,TimeUnit.SECONDS));
        queue.add(new Item("大碗稀饭4",0,TimeUnit.SECONDS));

        System.out.println("begin time:" + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        for (int i = 0; i < 4; i++) {
            Item take = queue.take();
            System.out.format("name:{%s}, time:{%s}\n",take.name, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
        }

    }
}

class Item implements Delayed{

    public String name;
    /*
    设置过期时间
     */
    public long time;

    public Item (String name, long time, TimeUnit timeUnit) {
        this.name = name;
        this.time = System.currentTimeMillis() + timeUnit.toMillis(time);
    }

    @Override
    public long getDelay (TimeUnit unit) {
        long delay = time - System.currentTimeMillis();
        return delay;
    }

    @Override
    public int compareTo (Delayed o) {
        Item item = (Item)o;
        int val = (int) (this.time - item.time);
        return val < 0 ? -1 : 1;
    }

    @Override
    public String toString () {
        return "Item{" +
                "name='" + name + '\'' +
                ", time=" + time +
                '}';
    }
}

使用TreeSet来实现延时队列

我们思考一下,我们可不可以使用其他方式来实现延时队列呢,是可以的,实现延时队列的两个条件,一个是排序,而是取元素的时候判断是否需要等待


import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Iterator;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class TreeSetTest {
    public static void main (String[] args) {
        TreeSet<Item> set = new TreeSet<>();

        set.add(new Item("大碗稀饭1",5,TimeUnit.SECONDS));
        set.add(new Item("大碗稀饭2",3,TimeUnit.SECONDS));
        set.add(new Item("大碗稀饭3",2,TimeUnit.SECONDS));
        set.add(new Item("大碗稀饭4",10,TimeUnit.SECONDS));

        System.out.println("begin time:" + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

        ReentrantLock lock = new ReentrantLock();//锁
        Condition condition = lock.newCondition();

        Iterator<Item> iterator = set.iterator();
        while(iterator.hasNext()){
            lock.lock();
            try {
                Item item = iterator.next();
                for (; ; ) {
                    long delay = item.getDelay();
                    if (delay <= 0) {
                        System.out.format("message:{%s}, time:{%s}\n",item.message, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
                        break;
                    } else {
                        condition.awaitNanos(delay);//有限等待
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

class Item implements Comparable{

    public String message;
    public long time;

    public Item (String message, long time, TimeUnit timeUnit) {
        this.message = message;
        this.time = System.currentTimeMillis() + timeUnit.toMillis(time);
    }

    /**
     * 计算延迟时间
     */
    public long getDelay(){
        long delay = time - System.currentTimeMillis();
        return delay;
    }

    @Override
    public int compareTo (Object o) {
        Item item = (Item)o;
        int val = (int) (this.time - item.time);
        return val <= 0 ? -1 : 1;
    }

    @Override
    public String toString () {
        return "Item{" +
                "message='" + message + '\'' +
                ", time=" + time +
                '}';
    }
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值