如何实现延迟队列

1,196 阅读6分钟

延迟队列

应用场景

下单后,30分钟内未付款就自动取消订单等; 支付后,24小时未评论自动好评;

基于java延时队列DelayQueue实现

DelayQueue是一个BlockingQueue(无界阻塞)队列,它本质就是封装了一个PriorityQueue(优先队列),我们在向DelayQueue队列中添加元素时,会给元素一个Delay(延迟时间)作为排序条件,队列中最小的元素会优先放在队首。队列中的元素只有到了Delay时间才允许从队列中取出。

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

基于定时任务实现

(1) 可以利用Scheduled注解, java服务在运行时, 不断执行定时任务, 一旦java服务不在运行状态, 定时任务就会失效

(2) 利用单独部署的任务中心, 来执行任务调度.

基于redis zset实现

Redis由于其自身的Zset数据结构,也同样可以实现延时的操作。 Zset本质就是Set结构上加了个排序的功能,除了添加数据value之外,还提供另一属性score,这一属性在添加元素时候可以指定,每次指定score后,Zset会自动重新按新的值调整顺序。

(1) 如果score代表的是想要执行时间的时间戳,在某个时间将它插入Zset集合中,它会按照时间戳大小进行排序,也就是对执行时间前后进行排序。

往delay_queue这个zset集合中, 添加一个分值为100的, 叫小明的对象 ZADD delay_queue 100 小明

(2) 不断地进行取第一个key值,如果当前时间戳大于等于该key值的socre就将它取出来进行消费删除,就可以达到延时执行的目的。 注意不需要遍历整个Zset集合,以免造成性能浪费。

ZRANGE delay_queue 0 0 withscores 查询得到delay_queue这个zset集合中, 按照分值升序的第一个元素的对象以及分值.

ZREM delay_queue 小明 根据对象名 移除对象和分值

注意这里的查询判断和移除可以用LUA脚本来保证一起执行.

需要一个线程不断的去访问消息zset集合

redis 的过期回调

Redis的key过期回调事件,也能达到延迟队列的效果,简单来说我们开启监听key是否过期的事件,一旦key过期会触发一个callback事件。

需要两个操作

(1) 创建RedisListenerConfig作为配置bean, 添加到容器中

(2) 创建事件监听类RedisKeyExpirationListener, 当redis某个key失效后, 就会触发RedisKeyExpirationListener中的回调方法.

RedisKeyExpirationListener在继承时, 需要传入RedisListenerConfig类

RabbitMQ 实现延迟队列

RabbitMQ支持为消息设置过期时间, 当消息过期后, 会被探测到, 然后可以路由转发到另一个消息队列中.

因此我们只需要两个队列, 第一个队列作为延迟队列, 专门用来让消息过期, 然后这些过期的消息被探测到后, 自动转发到另一个消息队列中被正常消费.

TTL: 指的是消息的存活时间,RabbitMQ可以通过x-message-tt参数来设置指定Queue(队列)和 Message(消息)上消息的存活时间,它的值是一个非负整数,单位为微秒。

DLX: 即死信交换机,绑定在死信交换机上的即死信队列。RabbitMQ的Queue(队列)可以配置两个参数x-dead-letter-exchange和x-dead-letter-routing-key(可选),一旦队列内出现了Dead Letter(死信),则按照这两个参数可以将消息重新路由到另一个Exchange(交换机),让消息重新被消费。

时间轮实现延迟队列

这里通过介绍kafka的时间轮的时间, 来介绍时间轮是个什么东西, 其实本质就是一个定时任务触发的功能, 目前java没有现成的时间轮的库, 如果需要使用, 需要自己实现.

kafka的时间轮

blog.csdn.net/u013256816/… zhuanlan.zhihu.com/p/121483218

kafka本身就支持延迟生产消息, 延迟拉取消息, 延迟删除消息等功能, Kafka 并没有使用JDK自带的Timer 或DelayQueue来实现延时的功能,而是基于时间轮的概念自定义实现了一个用于延时功能的定时器(SystemTimer)。 时间轮的应用并非Kafka独有,其应用场景还有很多,在Netty、Akka、Quartz、Zookeeper等组件中都存在时间轮的踪影。

时间轮是多层的, 第一层的时间轮是真正被持有的时间轮, 其它高层的时间轮是由底层的时间轮创建被持有的.

第一层时间轮, 每一个间隔是1ms, 整个时间轮的大小为固定数目, 假设为20, 那么第一层时间轮就能囊括0 - 20s, 然后第二层时间轮每一个间隔就是第一层时间轮的整体时间, 即为20s, 同时第二层也是大小为20, 第二层就能囊括0 - 400ms, 第三层的话, 每一个间隔就是400ms.

第一层时间轮的0位置就是currentTime, 然后整体时间轮囊括的时间范围就是currentTime + 最高一层时间轮的范围.

具体的定时任务, 就会被插入到时间轮的对应的间隔中, 每个间隔会保存一个双向链表, 链表的每个节点就是一个定时任务. 当高层的时间轮, 推进到当前间隔的时候, 需要执行对应时间间隔的链表任务, 当发现定时任务的时间还没到的时候, 会对该任务降级, 放入到底层的时间轮中, 因此最终真正执行的定时任务, 一定是在第一层时间轮被检查到的.

参考链接: www.jianshu.com/p/837ec4ea9…

时间轮上可能很多间隔都是没有定时任务的, 如果时间轮一直一个间隔一个间隔的运行的话, 其实有很多情况都是在空推进.为了解决这个问题, kafka额外维护了一个jdk的延迟队列delayQueue, 延迟队列中保存的是时间轮中的每个间隔, 每个间隔会有个延迟时间, 延迟队列会对间隔进行排序,

在新建定时任务的时候, 会往对应间隔中添加定时任务, 同时也会将该间隔添加到延迟队列中delayQueue.

会有一个线程, 不断的从延迟队列中, 获取头部的间隔, 然后判断其中的定时任务是否到期, 如果到期了, 要么将一些任务进行时间轮的降级, 要么就说明该任务到期了(例如第一层时间轮的间隔), 就直接执行该任务.

然后推进时间轮的指针, 即推动时间轮的时间.

时间轮的优点

(1) 相比于常用的DelayQueue的时间复杂度O(logN),TimingWheel的数据结构在插入任务时只要O(1),获取到达任务的时间复杂度也远低于O(logN)。

(2) 同时多层时间轮的设计, 对于时间很长的定时任务, 也能完成很好的映射, 只需要5层时间轮,可表示的时间跨度已经长达24年(216000小时)。