【Java笔记】实现延时队列1:JDK DelayQueue
文章目录
- 需求
- 创建订单类
- 创建延时队列
- 优缺点
- Reference
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 secondsprivate long timeout;public CancelOrder(String orderNo, long timeout) {this.orderNo = orderNo;this.timeout = timeout + System.nanoTime();}@Overridepublic String toString() {return "CancelOrder{" +"orderNo='" + orderNo + '\'' +", timeout=" + timeout +'}';}@Overridepublic long getDelay(TimeUnit unit) {// 以JVM高分辨率时间源的值为参考,获取过期时刻return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);}@Overridepublic 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专门处理,效率低
Reference
订单超时自动取消的技术方案解析及代码实现