完整实现-通过DelayQueue实现延时任务

可访问
• 阅读 1448

实现延时任务有很多的方法,网上关于延时任务的实现的文章已经不少了。比如:实现延时任务的10种方法等等。但是这些文章基本上都是将方法大概的列举一下,给出部分示例代码,对于有经验的老程序员可能一看就知道该怎么去把它实现完整,但是对于初学者来说不够友好。所以,我打算写一个系列的文章,详细的给出每种延时任务的实现方法、完整实现代码,以及工作原理,欢迎并期待大家关注我

小概念:什么是延时任务?举个例子:你买了一张火车票,必须在30分钟之内付款,否则该订单被自动取消。订单30分钟不付款自动取消,这个任务就是一个延时任务。

一、DelayQueue的应用原理

DelayQueue是一个无界的BlockingQueue的实现类,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。

  • BlockingQueue即阻塞队列,java提供的面向多线程安全的队列数据结构,当队列内元素数量为0的时候,试图从队列内获取元素的线程将被阻塞或者抛出异常。
  • 这里的“无界”队列,是指队列的元素数量不存在上限,队列的容量会随着元素数量的增加而扩容。

完整实现-通过DelayQueue实现延时任务
DelayQueue实现了BlockingQueue接口,所以具有无界、阻塞的特点,除此之外它自己的核心特点就是:

  • 放入该队列的延时任务对象,只要到达延时时间之后才能被取到
  • DelayQueue 不接收null元素
  • DelayQueue 只接受那些实现了java.util.concurrent.Delayed接口的对象

二、订单延时任务的实现

了解了DelayQueue的特点之后,我们就可以利用它来实现延时任务了,实现java.util.concurrent.Delayed接口。

import org.jetbrains.annotations.NotNull;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * 延时订单任务
 */
public class OrderDelayObject implements Delayed {
  private String name;
  private long delayTime;   //延时时间
  //实际业务中这里传订单信息对象,我这里只做demo,所以使用字符串了
  private String order;

  public OrderDelayObject(String name, long delayTime, String order) {
    this.name = name;
    //延时时间加上当前时间
    this.delayTime = System.currentTimeMillis() + delayTime;
    this.order = order;
  }

  //获取延时任务的倒计时时间
  @Override
  public long getDelay(TimeUnit unit) {
    long diff = delayTime - System.currentTimeMillis();
    return unit.convert(diff, TimeUnit.MILLISECONDS);
  }

  //延时任务队列,按照延时时间元素排序,实现Comparable接口
  @Override
  public int compareTo(@NotNull Delayed obj) {
    return Long.compare(this.delayTime, ((OrderDelayObject) obj).delayTime);
  }

  @Override
  public String toString() {
    Date date = new Date(delayTime);
    SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    return "\nOrderDelayObject:{"
            + "name=" + name
            + ", time=" + sd.format(date)
            + ", order=" + order
            + "}";
  }
} 
  • 上文类中的order为订单信息对象,在实际的业务开发过程中应该是传递订单信息,用于取消订单业务的实现(订单30分钟不付款自动取消)。
  • Delayed接口继承自 Comparable接口,所以需要实现compareTo方法,用于延时任务在队列中按照“延时时间”进行排序。
  • getDelay方法是Delayed接口方法,实现该方法提供获取延时任务的倒计时时间

三、订单处理

首先我们需要一个容器,永久保存延时任务队列,如果是Spring开发环境我们可以这样做。

@Bean("orderDelayQueue")
public DelayQueue<OrderDelayObject> orderDelayQueue(){
    return new DelayQueue<OrderDelayObject>();
}

当用户下单的时候,将订单下单任务放入延时队列

@Resource
private DelayQueue<OrderDelayObject> orderDelayQueue;

//发起订单下单的时候将订单演示对象放入orderDelayQueue
orderDelayQueue.add(
        new OrderDelayObject(
                "订单延时取消任务",
                30 * 60 * 1000,    //延时30分钟
                "延时任务订单对象信息"
        )
);

系统内开启一个线程,不断的从队列中获取消息,获取到之后对延时消息进行处理。DelayQueue的take方法从队列中获取延时任务对象,如果队列元素数量为0,或者没有到达“延时时间的任务”,该线程会被阻塞。

@Component
public class DelayObjectConsumer<OrderDelayObject> implements InitializingBean {

  @Resource
  private DelayQueue<OrderDelayObject> orderDelayQueue;

  @Override
  public void afterPropertiesSet() throws Exception {
    new Thread(() -> {
      while (true) {
        OrderDelayObject task = orderDelayQueue.take();
        System.out.println(task.toString());
        System.out.println(task.getOrder());
        //根据order订单信息,去查询该订单的支付信息
        //如果用户没有进行支付,将订单从数据库中关闭
        //如果订单并发量比较大,这里可以考虑异步或线程池的方式进行处理
      }
    }).start();
  }
}

需要说明的是,这里的while-true循环的延时任务处理时顺序执行的,在订单并发量比较大的时候,需要考虑异步处理的方式完成订单的关闭操作。我之前写作一个SpringBoot的可观测、易配置的线程池开源项目,可能会对你有帮助,源代码地址:https://gitee.com/hanxt/zimug...
经过我的测试,放入orderDelayQueue的延时任务,在半小时之后得到正确的执行处理。说明我们的实现是正确的。

四、优缺点

使用DelayQueue实现延时任务非常简单,而且简便,全部都是标准的JDK代码实现,不用引入第三方依赖(不依赖redis实现、消息队列实现等),非常的轻量级。

它的缺点就是所有的操作都是基于应用内存的,一旦出现应用单点故障,可能会造成延时任务数据的丢失。如果订单并发量非常大,因为DelayQueue是无界的,订单量越大,队列内的对象就越多,可能造成OOM的风险。所以使用DelayQueue实现延时任务,只适用于任务量较小的情况。
欢迎关注我的公告号:字母哥杂谈,回复003赠送作者专栏《docker修炼之道》的PDF版本,30余篇精品docker文章。字母哥博客:zimug.com

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
STM32延时函数的四种方法
单片机编程过程中经常用到延时函数,最常用的莫过于微秒级延时delay\_us()和毫秒级delay\_ms()。本文基于STM32F207介绍4种不同方式实现的延时函数。1、普通延时这种延时方式应该是大家在51单片机时候,接触最早的延时函数。这个比较简单,让单片机做一些无关紧要的工作来打发时间,经常用循环来实现,在某些编译器下,代码会被
一种异步延迟队列的实现方式
目前系统中有很多需要用到延时处理的功能:支付超时取消、排队超时、短信、微信等提醒延迟发送、token刷新、会员卡过期等等。通过延时处理,极大的节省系统的资源,不必轮询数据库处理任务。今天,就来介绍一种异步延迟队列的实现方式
灯灯灯灯 灯灯灯灯
4年前
阿里二面必备考题之Java并发!全面解析
一、使用线程有三种使用线程的方法:实现Runnable接口实现Callable接口继承Thread类实现Runnable和Callable接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过Thread来调用。可以理解为任务是通过线程驱动从而执行的。实现Runnable接口cpublicclass
待兔 待兔
4年前
mysql面试题:如何实现 MySQL 的读写分离?MySQL 主从复制原理是啥?如何解决 MySQL 主从同步的延时问题?
你有没有做MySQL读写分离?如何实现MySQL的读写分离?MySQL主从复制原理的是啥?如何解决MySQL主从同步的延时问题?考点分析高并发这个阶段,肯定是需要做读写分离的,啥意思?因为实际上大部分的互联网公司,一些网站,或者是app,其实都是读多写少。所以针对这个情况,就是写一个主库,但是主库挂多个从库,然后从多个从库来
@Transaction注解的失效场景
事情是这样,最近在实现一个需求的时候,有一个定时异步任务会捞取主表的数据并置为处理中(为了防止任务执行时间过长,下次任务执行把本次数据重复捞取),然后根据主表关联明细表数据,然后将明细表数据进行组装,等待所有明细数据处理完成之后,将主表状态置为完成;大概当时的代码示例(只是截取部分)如下:
Wesley13 Wesley13
3年前
CRC校验查表法原理及实现(CRC
绪论在网上浏览了很多关于CRC校验的文章,基本上都是针对CRC校验原理的阐述以及关于CRC校验查表法的实际应用以及具体软件实现方法。_至于查的表是怎么来的,软件为什么要这样实现很多文章并没有说明_。本篇文章就针对这两点问题进行总结和归纳,有错误的地方欢迎大家评论区指出,不胜感激。_注意:本篇文章不涉及CRC校验的基本原理,如
Wesley13 Wesley13
3年前
unity延时功能的几种实现
 原文:https://www.cnblogs.com/xifarm/p/invoke.html在Unity3D中,新建的脚本会默认继承MonoBehaviour,其中Update函数会被引擎自动调度,如在我的电脑上,Update函数被调用的周期为0.012s。那么Unity3D中,仅有通过Update函数来实现延时功能吗? 还有无其他的实现呢?
Easter79 Easter79
3年前
Spring的业务层和Web层
任务调度  quartz框架  quartz框架实现了Spring的任务调度,用户可以随意的定义触发器调度时间表,并将触发器和任务进行映射。quartz通过调度器、触发器和任务实现任务调度。  Job:主要用来设计任务实现的逻辑,并且只有一个方法execute。  JobDetail:主要用来通过newInst
Stella981 Stella981
3年前
RocketMQ源码 — 九、 RocketMQ延时消息
上一节(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fwww.cnblogs.com%2Fsunshine2015%2Fp%2F9011446.html)消息重试里面提到了重试的消息可以被延时消费,其实除此之外,用户发送的消息也可以指定延时时间(更准确的说是延时等级),然后在指定延时时
Wesley13 Wesley13
3年前
urllib在某些情况下性能低于urllib2一例
再一次尝试将QQ空间的头像保存到本地的过程中,发现每次执行那段代码都要有大约将近20s左右的延时。这个延时对于正常来说是不可忍受的。尝试解决之。首先尝试用浏览器直接打开头像地址,发现没有任何延时,瞬间即开。看来问题是出现在了代码之上。为了方便,获取头像采用的是urllib.urlretrive方法。既然这个方法有问题,那用最原始的的urllib.open
眼疾手快 眼疾手快
1年前
如何使用Selenuim浏览器自动化框架实现自动登录社交媒体账号和自动发布文章
在当今社交媒体盛行的时代,程序员们经常需要在不同的平台上自动执行一些任务,比如登录社交媒体账号并发布文章。本文将介绍如何利用Selenium浏览器自动化框架实现这一任务,同时结合万媒易发多平台内容同步助手,提高文章发布的效率。技术栈为了实现自动登录社交媒体