【Java笔记】实现延时队列1:JDK DelayQueue


JDK DelayQueue是一个无阻塞队列,底层是 PriorityQueue

需求

经典的订单超时取消

创建订单类

放入DelayQueue的对象需要实现Delayed接口

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

可以看到,Delayed包含一个getDelay抽象方法,同时继承了Comparable<Delayed>接口,因此要实现Delayed接口需要实现getDelayComparable<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专门处理,效率低

Reference

订单超时自动取消的技术方案解析及代码实现

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值