为什么需要定时任务?

我们来看几个非常常见的业务场景:

1.某系统凌晨要进行数据备份。
2.某电商平台,用户下单半小时未支付的情况下需要自动取消订单
3.某媒体聚合平台,每十分钟动态抓取某网站的数据为自己所用。
4.某博客平台,支持定时发送文章
5.某基金平台,每晚定时计算用户当日收益情况并推送给用户最新的数据。

这些场景往往都要求我们在某个特定的时间取做某个事情。

单机定时任务技术选型

Timer

java.util.Timer是JDK1.3开始就已经支持的一种定时任务的实现方式。

Timer内部使用一个叫做TaskQueue的类存放定时任务,它是一个基于最小堆实现的优先级队列。TaskQueue会按照任务距离下一次执行时间的大小将任务排序,保证在堆顶的任务最先执行。这样在需要执行任务时,每次只需要取出堆顶的任务运行即可!

Timer使用起来比较简单,通过下面的方式我们就能创建一个1s之后执行的定时任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 示例代码:
TimerTask task = new TimerTask() {
public void run() {
System.out.println("当前时间: " + new Date() + "n" +
"线程名称: " + Thread.currentThread().getName());
}
};
System.out.println("当前时间: " + new Date() + "n" +
"线程名称: " + Thread.currentThread().getName());
Timer timer = new Timer("Timer");
long delay = 1000L;
timer.schedule(task, delay);


//输出:
当前时间: Fri May 28 15:18:47 CST 2021n线程名称: main
当前时间: Fri May 28 15:18:48 CST 2021n线程名称: Timer

不过其缺陷较多,比如一个Timer一个线程,这就导致Timer的任务的执行只能串行执行,一个任务执行时间过长的话会影响其他任务(性能非常差),再比如发生异常时任务直接停止(Timer只捕获了InterruptedException)。

ScheduledExecutorService

ScheduledExecutorService是一个接口,有多个实现类,比较常用的是ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor本身就是一个线程池,支持任务并发执行。并且,其内部使用DelayQueue作为任务队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 示例代码:
TimerTask repeatedTask = new TimerTask() {
@SneakyThrows
public void run() {
System.out.println("当前时间: " + new Date() + "n" +
"线程名称: " + Thread.currentThread().getName());
}
};
System.out.println("当前时间: " + new Date() + "n" +
"线程名称: " + Thread.currentThread().getName());
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
long delay = 1000L;
long period = 1000L;
executor.scheduleAtFixedRate(repeatedTask, delay, period, TimeUnit.MILLISECONDS);
Thread.sleep(delay + period * 5);
executor.shutdown();
//输出:
当前时间: Fri May 28 15:40:46 CST 2021n线程名称: main
当前时间: Fri May 28 15:40:47 CST 2021n线程名称: pool-1-thread-1
当前时间: Fri May 28 15:40:48 CST 2021n线程名称: pool-1-thread-1
当前时间: Fri May 28 15:40:49 CST 2021n线程名称: pool-1-thread-2
当前时间: Fri May 28 15:40:50 CST 2021n线程名称: pool-1-thread-2
当前时间: Fri May 28 15:40:51 CST 2021n线程名称: pool-1-thread-2
当前时间: Fri May 28 15:40:52 CST 2021n线程名称: pool-1-thread-2

不论是使用Timer还是ScheduledExecutorService都无法使用Cron表达式指定任务执行的具体时间。

Spring Task

我们直接通过Spring提供的@Scheduled注解可以定义定时任务,非常方便

1
2
3
4
5
6
7
/**
* cron:使用Cron表达式。 每分钟的1,2秒运行
*/
@Scheduled(cron = "1-2 * * * * ? ")
public void reportCurrentTimeWithCronExpression() {
log.info("Cron Expression: The time is now {}", dateFormat.format(new Date()));
}

Spring Task是支持Cron表达式的。Cron表达式主要用于定时作业(定时任务)系统定义执行时间或执行频率的表达式,非常厉害,你可以通过Cron表达式进行设置定时任务每天或者每个月什么时候执行等等操作。推荐一个Cron表达式生成器:http://cron.qqe2.com/

但是,Spring自带的定时调度只支持单机,并且提供的功能比较单一。

分布式定时任务技术选型

上面提到的一些定时任务的解决方案都是在单机下执行的,使用于比较简单的定时任务场景比如每天凌晨备份一次数据。

二u给我们需要一些高级特性比如支持任务在分布式场景下的分片和高可用的话,我们就需要用到分布式任务调度框架了。

通常情况下,一个定时任务的执行往往涉及到下面这些角色:

  • **任务:**首先肯定是要执行的任务,这个任务就是具体的业务逻辑比如定时发送文章。
  • **调度器:**其次是调度中心,调度中心主要负任务管理,会分配任务给执行器。
  • 执行器::最后就是执行器,执行器接受调度器分派的任务并执行。

Quartz

一个很火的开源任务调度框架,完全由Java写成。Quartz可以说是Java定时任务领域的老大哥或者说参考标准了,其他的任务调度框架基本都是基于Quartz开发的,比如当当网的elastic-job就是基于quartz二次开发之后的分布式调度解决方案。

使用Quartz可以很方便地与Spring继承,并且支持动态添加任务和集群。但是,Quartz使用起来也比较麻烦,API繁琐。

并且,Quartz并没有内置UI管理控制台,不过可以使用quartzui来解决这个问题。

另外,Quartz虽然也支持分布式任务。但是,他是在数据库层面,通过数据库的锁机制做的,有非常多的弊端比如系统侵入性严重,节点负载不均衡。

总结:

  • 优点:可以与Spring集成,并且支持动态添加任务和集群。
  • 缺点:分布式支持不友好,没有内置UI管理控制台、使用麻烦。

Elastic-Job

Elastic-Job是当当网开源的一个基于QuartzZooKeeper的分布式调度解决方案,由两个相互独立的子项目Elastic-Job-LiteElastic-Job-Cloud组成,一般我们只要使用Elastic-Job-Lite就好。

ElasticJob支持任务在分布式场景下的分片和高可用、任务可视化管理等功能。

架构设计如图所示:

从上图可以看出,Elastic-Job没有调度中心这一概念,而是使用Zookeeper作为注册中心,注册中心负责协调分配任务到不同的节点上。

Elastic-Job中的定时调度都是由执行器自行触发,这种设计也被称为去中心化设计

总结:

  • **优点:**可以与Spring集成、支持分布式、支持集群、性能不错
  • **缺点:**依赖了额外的中间件比如Zookeeper(复杂度增加,可靠性降低、维护成本变高)

XXL-Job

XXL-Job于2015年开源,是一款优秀的轻量级分布式任务调度框架,支持任务可视化管理、弹性扩容、任务失败重试和告警、任务分片等功能,

XXL-JOB的架构设计如图所示:

从上图可以看出,XXL-JOB调度中心执行器两大部分组成。调度中心主要负责任务管理、执行器管理以及日志管理。执行器主要是接受调度信号并处理。另外,调度中心进行任务调度时,是通过自研RPC来实现的。

不同于Elastic-Job的去中心化设计,XXL-JOB的这种设计也被称为中心化设计。

Quartz类似XXL-JOB也是基于数据库锁调度任务,存在性能瓶颈。不过,一般在任务量不是特别大的情况下,没有什么影响的,可以满足大部分需求。

使用的话,只需要重写IJobHandler自定义任务执行逻辑就可以了,

1
2
3
4
5
6
7
8
9
10
@JobHandler(value="myApiJobHandler")
@Component
public class MyApiJobHandler extends IJobHandler {

@Override
public ReturnT<String> execute(String param) throws Exception {
//......
return ReturnT.SUCCESS;
}
}

还可以直接基于注解定义任务。

1
2
3
4
5
@XxlJob("myAnnotationJobHandler")
public ReturnT<String> myAnnotationJobHandler(String param) throws Exception {
//......
return ReturnT.SUCCESS;
}

总结:

  • **优点:**开箱即用(学习成本比较低)、与Spring集成、支持分布式、支持集群、内置了UI管理控制台。
  • **缺点:**不支持动态添加任务