JDK
DelayQueue
是一个无阻塞队列,底层是
PriorityQueue
需求
经典的订单超时取消
创建订单类
放入DelayQueue
的对象需要实现Delayed
接口
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
可以看到,Delayed
包含一个getDelay
抽象方法,同时继承了Comparable<Delayed>
接口,因此要实现Delayed
接口需要实现getDelay
和Comparable<Delayed>
两个抽象方法,最后完成订单类CancelOrder
,实现Delayed
接口:
public class CancelOrder implements Delayed {
// 订单号
private String orderNo;
// 过期时间 nano seconds
private long timeout;
public CancelOrder(String orderNo, long timeout) {
this.orderNo = orderNo;
this.timeout = timeout + System.nanoTime();
}
@Override
public String toString() {
return "CancelOrder{" +
"orderNo='" + orderNo + '\'' +
", timeout=" + timeout +
'}';
}
@Override
public long getDelay(TimeUnit unit) {
// 以JVM高分辨率时间源的值为参考,获取过期时刻
return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
if (o == this){
return 0;
}
CancelOrder t = (CancelOrder) o;
long d = (getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS));
return (d == 0) ? 0 : ((d < 0)? -1 : 1);
}
}
这里有几个地方需要啰嗦下:
- 订单类
CancelOrder
包含两个成员属性:orderNo
:订单编号timeout
:过期时间,单位为纳秒(1ns = 10^-9s),所以要用long
getDelay()
方法:用于获取订单过期的时刻,订单过期时刻是以JVM的时间作为起点计算的System.nanoTime()
: 返回正在运行的Java虚拟机的高分辨率时间源的当前值,以纳秒计- 订单过期的时刻 =JVM时间源的当前值+过期时间
timeout
compareTo
方法:就是实现了优先队列的比较方法,根据各个订单的过期时刻排序,这里其实就是一个小顶堆,队头为过期时刻最小的订单。
创建延时队列
创建一个DelayQueue
其实就跟创建PriorityQueue
差不多,只不过这里不需要重写个Comparator
,因为订单对象已经重写了CompareTo
了,是一个队头为最早过期(过期时刻最小的)元素的小顶堆。
下面主要用到DelayQueue
的两个方法,分别用于加入/取出订单:
put()
: 非常亲切的入队方法,跟普通队列一样,将对象加入队尾;take()
: 取出队头【过期】对象(不是跟poll()
一样直接取出了)
弄个测试类测试一下
public class DelayQueueTest {
public static void main(String[] args) throws InterruptedException {
DelayQueue<CancelOrder> queue = new DelayQueue<>();
// 每秒生成1个订单,共生成5个订单
for (int i = 0; i < 5; i++){
// 10s过期
CancelOrder cancelOrder = new CancelOrder("orderNo"+i, TimeUnit.NANOSECONDS.convert(10, TimeUnit.SECONDS));
// 获取当前时间
String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println(time + ": 生成订单, 10s有效,order: " + cancelOrder);
// 将订单放入延时队列
queue.put(cancelOrder);
// 控制每秒生成一个订单
Thread.sleep(1000);
}
// 延时队列取出超时订单
try {
while (!queue.isEmpty()){
// 轮询获取队头过期元素
CancelOrder order = queue.take();
// 获取当前时间
String timeout = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println(timeout + ": 订单超时,order:"+order);
}
} catch (InterruptedException e){
throw new RuntimeException(e);
}
}
}
输出:
2024-03-30 18:54:37: 生成订单, 10s有效,order: CancelOrder{orderNo='orderNo0', timeout=636498762218800}
2024-03-30 18:54:38: 生成订单, 10s有效,order: CancelOrder{orderNo='orderNo1', timeout=636499784320900}
2024-03-30 18:54:39: 生成订单, 10s有效,order: CancelOrder{orderNo='orderNo2', timeout=636500788490700}
2024-03-30 18:54:40: 生成订单, 10s有效,order: CancelOrder{orderNo='orderNo3', timeout=636501792751100}
2024-03-30 18:54:41: 生成订单, 10s有效,order: CancelOrder{orderNo='orderNo4', timeout=636502796614500}
2024-03-30 18:54:47: 订单超时,order:CancelOrder{orderNo='orderNo0', timeout=636498762218800}
2024-03-30 18:54:48: 订单超时,order:CancelOrder{orderNo='orderNo1', timeout=636499784320900}
2024-03-30 18:54:49: 订单超时,order:CancelOrder{orderNo='orderNo2', timeout=636500788490700}
2024-03-30 18:54:50: 订单超时,order:CancelOrder{orderNo='orderNo3', timeout=636501792751100}
2024-03-30 18:54:51: 订单超时,order:CancelOrder{orderNo='orderNo4', timeout=636502796614500}
优缺点
优点:
- 简单,不需要借助其他第三方组件,成本低,适合单体应用
缺点:
- 不适合数据量较大的场景:所有可能超时的数据都要进入
DelayQueue
中,全部保存在JVM内存中,内存开销大,可能引发内存溢出 - 无法持久化:因为存在JVM内存中,不像Redis可以通过AOF或者RDB,宕机数据就丢失了
- 无法较好地适配分布式集群:真要分布式就只能在集群中选一台leader专门处理,效率低