Java 延时队列,简单使用方式
前言:首先我们要知道延时队列是什么?可以用来干什么?
是什么?
看名称
队列
可想而知先进先出
的一个集合。但我们的延时队列并没有完全的遵循这个理念。
DelayQueue
内部其实是基于我们的优先队列
来实现的,也就是元素的先后顺序是按元素的Comparable
接口提供的顺序来出队
的。
DelayQueue
内部的元素必须是Delayed
的的实现类 而Delayed
的父接口是Comparable<Delayed>
。
Delayed
接口中需要我们实现一个long getDelay(TimeUnit unit);
元素的剩余时间TTL
。
干什么?
- 定时任务。
- 重试(5秒重试)。
- 延时通知。
都可以基于延时队列来实现。
开发中遇到一个这样的需求。
需要一个集合来维护热点数据,但这个热点数据是有时效性的,我们在去查询的时候先要判断下这个热点数据不存在,或者不在有效期内的。才能走剩下的逻辑。
当然这个需求听起来并不复杂。
我们自己实现的话。
是不是需要一个有顺序的集合?
是不是还每个元素都有一个有效期?
是不是还得保证元素出队的顺序?
如果说还有另外一个地方需要复用,我们是不是还得再写一套这样的逻辑。
所以,我们可以直接使用
DelayQueue
来实现这个需求,他天生就能保证我们的集合是有顺序的,并且保证过期的元素不在集合内。
简单使用
延时对象
java
@Data
static class SimpleDelayed<T> implements Delayed {
/**
* 对象创建时间
*/
@JsonIgnore
private final Date createDate;
/**
* 延时数(单位毫秒)
*/
private long delayCount;
private T data;
public SimpleDelayed(T data, long delayCount) {
this.data = data;
this.delayCount = delayCount;
this.createDate = new Date();
}
@Override
public long getDelay(TimeUnit unit) {
long diff = this.createDate.getTime() + this.delayCount - System.currentTimeMillis();
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed delayed) {
return Long.compare(this.getDelay(TimeUnit.MILLISECONDS),delayed.getDelay(TimeUnit.MILLISECONDS));
}
}
注意:compareTo
提供的顺序必须要与getDelay
否则会出现元素过期了但是在队列中。因为DelayQueue
内部维护的是一个优先队列
。所以必须保证这两顺序是一致的。所以这里的compareTo
直接比较了这两的剩余过期数
越小的越先出队。反之会出现,延时时间
已过,但是没有出队。compareTo
靠前,但是过期数未过的数卡着。
延时队列工具类
java
@Slf4j
public class SimpleDelayQueueUtils {
/**
* 1.定义了一个延时队列 队列中元素是我们的SimpleDelayed
*/
private final static DelayQueue<SimpleDelayed> SIMPLE_DELAYED_DELAY_QUEUE = new DelayQueue<>();
static{
// 2.声明一个异步线程
CompletableFuture.runAsync(() -> {
// 3.通过自旋去出队已过期的元素
for (;;) {
try {
// 4.take() 对头元素出队。
log.info("元素已过期{}",SIMPLE_DELAYED_DELAY_QUEUE.take().getData());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 单个线程的线程池,为了不占用fork-join线程池
}, Executors.newSingleThreadExecutor());
}
public static void add(SimpleDelayed... delayeds) {
SIMPLE_DELAYED_DELAY_QUEUE.addAll(Arrays.asList(delayeds));
}
// 测试
public static void main(String[] args) {
SimpleDelayed<String> nonceDelayed = new SimpleDelayQueueUtils.SimpleDelayed<>("111",5001);
SimpleDelayed<String> nonceDelayed1 = new SimpleDelayQueueUtils.SimpleDelayed<>("222", 4500);
SimpleDelayQueueUtils.add(nonceDelayed,nonceDelayed1);
}
}
输出
sh
23:35:00.693 [pool-1-thread-1] INFO com.mfyuan.chenapioperation.util.SimpleDelayQueueUtils - 元素已过期222
23:35:01.189 [pool-1-thread-1] INFO com.mfyuan.chenapioperation.util.SimpleDelayQueueUtils - 元素已过期111
思考
这里的data
元素是可以灵活替换的,因为我这里的需求是涉及到队列中的元素是否过期。所以这些就已经足够了。
但是当我们把data
,替换成Runable
再把出队的的元素通过线程池的方式去调用则就实现了定时任务
。重试
也可以当做定时任务
来理解,就把当单做一个任务,如果任务执行失败了或者出现异常了,那么我们重新讲元素放入延时队列中即可。
后话
Redis来实现我这个功能,虽然可以实现但是有些地方实现的逻辑也是不简单。更别说引入redis
的开发成本,杀🐔不需要🐂刀。
Redis zset
是可以保证集合的元素有顺序的,zset
的ttl
是指这个zset
整体的过期时间。(不确定,)