延时队列的实现

1. 应用场景

1、订单成功后,在30分钟内没有支付,自动取消订单
2、外卖平台发送订餐通知,下单成功后60s给用户推送短信。
3、如果订单一直处于某一个未完结状态时,及时处理关单,并退还库存

2. 什么是延时队列

当接收到消息之后,我需要隔一段时间进行处理(相对于立马处理,它隔了一段时间,所以他叫延迟消息)。

2.1 原理

DelayQueue中的所有元素必须实现Delayed接口

/**
 * 一种混合风格的接口,用来标记那些应该在给定延迟时间之后执行的对象。
 * <p>
 * 此接口的实现必须定义一个 compareTo 方法,该方法提供与此接口的 getDelay 方法一致的排序。
 */
public interface Delayed extends Comparable<Delayed> {
    /**
     * 返回与此对象相关的剩余有效时间,以给定的时间单位表示.
     */
    long getDelay(TimeUnit unit);
}

可以看到,Delayed接口除了自身的getDelay方法外,还实现了Comparable接口。getDelay方法用于返回对象的剩余有效时间,实现Comparable接口则是为了能够比较两个对象,以便排序。

也就是说,如果一个类实现了Delayed接口,当创建该类的对象并添加到DelayQueue中后,只有当该对象的getDalay方法返回的剩余时间≤0时才会出队。
另外,由于DelayQueue内部委托了PriorityBlockingQueue对象来实现所有方法,所以能以堆的结构维护元素顺序,这样剩余时间最小的元素就在堆顶,每次出队其实就是删除剩余时间≤0的最小元素。

无法使用take或poll移除未到期的元素,也不会将这些元素作为正常元素对待。例如,size方法同时返回到期和未到期元素的计数。此队列不允许使null元素。

3. 延时队列的实现一 - DelayQueue 延时队列

DelayQueue 是一个 BlockingQueue(无界阻塞)队列,它本质就是封装了一个 PriorityQueue(优先队列),PriorityQueue 内部使用完全二叉堆来实现队列元素排序,当向 DelayQueue 队列中添加元素时,会给元素一个 Delay(延迟时间)作为排序条件,队列中最小的元素会优先放在队首。队列中的元素只有到了 Delay 时间才允许从队列中取出。队列中可以放基本数据类型或自定义实体类,在存放基本数据类型时,优先队列中元素默认升序排列,自定义实体类就需要根据类属性值比较计算了。

3.1 demo

先简单实现一下看看效果,添加三个 order 入队 DelayQueue,分别设置订单在当前时间的5秒、10秒、15秒后取消。
在这里插入图片描述

要实现 DelayQueue 延时队列,队中元素实现 Delayed 接口,接口里只有一个 getDelay 方法,用于设置延期时间。Order 类中 compareTo() 负责对队列中的元素进行排序。

public class Order implements Delayed {
     /**
     * 到期时间 
     */
    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private long time;
    String name;

    public Order(String name, long time, TimeUnit unit) {
        this.name = name;
        this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);
    }

    @Override
    public long getDelay(TimeUnit unit) {
        // 剩余时间= 到期时间-当前系统时间,系统一般是纳秒级的,所以这里做一次转换
        long d = unit.convert(time-System.currentTimeMillis(), TimeUnit.NANOSECONDS);
        return d;
    }
    @Override
    public int compareTo(Delayed o) {
         // 订单剩余时间-当前传入的时间= 实际剩余时间(单位纳秒)
        long d = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
        // 根据剩余时间判断等于0 返回1 不等于0
        // 有可能大于0 有可能小于0  大于0返回1  小于返回-1
        return (d == 0) ? 0 : ((d > 0) ? 1 : -1);
    }
}

DelayQueue 的 put 方法是线程安全的,因为 put 方法内部使用了 ReentrantLock 进行线程同步。DelayQueue 还提供了两种出队的方法 poll() 和 take()。poll() 为非阻塞获取,没有到期的元素直接返回 null;take() 为阻塞方式获取,没有到期的元素线程将会等待。

public class DelayQueueDemo {

    public static void main(String[] args) throws InterruptedException {
        Order Order1 = new Order("Order1", 5, TimeUnit.SECONDS);
        Order Order2 = new Order("Order2", 10, TimeUnit.SECONDS);
        Order Order3 = new Order("Order3", 15, TimeUnit.SECONDS);
        DelayQueue<Order> delayQueue = new DelayQueue<>();
        delayQueue.put(Order1);
        delayQueue.put(Order2);
        delayQueue.put(Order3);

        System.out.println("订单延迟队列开始时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        while (delayQueue.size() != 0) {
            //取队列头部元素是否过期
            Order task = delayQueue.poll();
            if (task != null) {
                System.out.format("订单:{%s}被取消, 取消时间:{%s}\n", task.name, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            }
            Thread.sleep(1000);
        }
    }
}

上边只是简单的实现入队与出队的操作,实际开发中会有专门的线程,负责消息的入队与消费。执行后看到结果如下,Order1、Order2、Order3 分别在 5秒、10秒、15秒后被执行,至此就用 DelayQueue 实现了延时队列。

订单延迟队列开始时间:2020-05-06 14:59:09
订单:{Order1}被取消, 取消时间:{2020-05-06 14:59:14}
订单:{Order2}被取消, 取消时间:{2020-05-06 14:59:19}
订单:{Order3}被取消, 取消时间:{2020-05-06 14:59:24}

4. 延时队列的实现二 - Redis + 定时

在这里插入图片描述

Redis 有一个有序集合的数据结构 ZSet,ZSet 中每个元素都有一个对应 Score,ZSet 中所有元素是按照其 Score 进行排序的。Redis 的 ZSet 实现延迟队列逻辑如下:

  1. 入队操作:zadd key timestamp task将需要处理的任务,按其需要延迟处理时间作为 Score 加入到 ZSet 中。Redis 的 zadd 的时间复杂度是 O(logN),能相对比较高效的进行入队操作。
  2. 定时(比如每隔一秒)通过 zrangebyscore(返回指定分数范围的升序元素)方法查询 ZSet 中 Score 最小的元素,具体操作为:zrangebyscore KEY -inf +inf limit 0 1 WITHSCORES。查询结果有两种情况:
    a.查询出的分数小于等于当前时间戳,说明到这个任务需要执行的时间了,则去异步处理该任务。
    b.查询出的分数大于当前时间戳,由于刚刚的查询操作取出来的是分数最小的元素,所以说明 ZSet 中所有的任务都还没有到需要执行的时间,则休眠一秒后继续查询。

来源: https://www.jianshu.com/p/977466020144
原理:https://blog.csdn.net/qq_35029061/article/details/86741925

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值