一、 为什么要定时任务?
常见的业务场景:
电商平台,用户下单半小时未支付自动取消订单。
媒体平台,每十分钟动态抓取其他网站数据为自己所用。
健康平台,每天定时获取个人健康数据。
二、单机定时任务技术选型
1 Timer
1.1 什么是Timer?
Timer
内部使用一个叫做TaskQueue的类存放定时任务,它基于最小堆实现的优先级队列。TaskQueue会按照任务距离下一次执行时间的大小将任务排序,保证在堆顶的任务最先执行。这样在执行任务时,每次只需要取出堆顶的任务即可。
/**
* TimeTest
*/
public static void timerTest() {
TimerTask task1 = new TimerTask() {
@Override
public void run() {
System.out.println("当前时间:" + new Date() + "n" +
"TimeTask线程名称1:" + Thread.currentThread().getName());
}
};
System.out.println("当前时间:" + new Date() + "n" +
"TimeTask线程名称2:" + Thread.currentThread().getName());
// 多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,
// 其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题
Timer timer = new Timer("Timer");
long delay = 1000L;
timer.schedule(task1, delay);
}
// 运行结果
当前时间:Thu Nov 18 10:12:27 CST 2021nTimeTask线程名称2:main
当前时间:Thu Nov 18 10:12:28 CST 2021nTimeTask线程名称1:Timer
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1.2 有什么有点和缺陷?
- 一个Timer一个线程会导致Timer任务执行只能串行执行,一个任务执行时间过长会严重影响其他任务的执行。
- 发生异常时任务直接停止,且只捕获InterruptedException。
- 无法使用Corn表达式指定任务具体时间。
2 ScheduledExecutorService
2.1 什么是ScheduledExecutorService?
ScheduledExecutorService
是一个接口,有多个实现类,比较常用的就是ScheduledThreadPoolExecutor
。ScheduledThreadPoolExecutor
本身是一个线程池,支持任务并发执行,其内部是使用DelayQueue作为任务队列。
/**
* scheduledExecutorServiceTest
*/
public static void scheduledExecutorServiceTest() {
TimerTask task = new TimerTask() {
/**
* The action to be performed by this timer task.
*/
@Override
public void run() {
System.out.println("当前时间:" + new Date() + "n" +
"scheduledExecutorServiceTest线程名称1:" + Thread.currentThread().getName());
}
};
System.out.println("当前时间:" + new Date() + "n" +
"scheduledExecutorServiceTest线程名称2:" + Thread.currentThread().getName());
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
long delay = 1000L;
long period = 1000L;
executor.scheduleAtFixedRate(task, delay, period, TimeUnit.MICROSECONDS);
try {
Thread.sleep(delay + period * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2.2 有什么有点和缺陷?
无法使用Corn表达式指定任务具体时间
3 Spring Task
直接使用
@Scheduled
注解即可定义定时任务。详情请参看《Spring Schedule 实现定时任务》
4 时间轮
时间轮就是一个环形队列(底层一般使用数组实现),队列中的每一个元素都可以存放一个定时任务列表。时间轮中每个时间格代表时间的基本时间跨度,加入时间一秒走一个时间格的话,则时间轮最高精度为1秒。
比如:下面这个12个时间格的时间轮,转完一圈需要12秒,当我们想创建一个3秒后执行的任务时,则只需将定时任务放置在下标为3的时间格中。
当我们想创建15秒后执行的任务时,就会产生(圈数/轮数)的概念,则需要将该任务放置在第二圈的下标为5的时间格中。
如上图该时间轮中,第一层(秒针)时间精度为1秒,一圈为20秒。第二层(分针)时间精度为20秒,转一圈为20X20=400秒。第三层(时针)时间精度为400秒,转一圈为400X20=8000秒。如果我们需要350秒后执行的任务,则该任务会被放置在第二层。(350/20=17.5,当秒针转17圈后,分针指针指向17,此时将第17格子的任务移动至第一层,秒针再走10秒(10格),共计350秒。将任务放置到第一层的第10个格子。
三、分布式定时任务技术选型
1 Quartz
一个很火的开源任务调度框架,完全由Java
写成。Quartz
可以说是 Java 定时任务领域的老大哥或者说参考标准,其他的任务调度框架基本都是基于 Quartz
开发的,比如当当网的elastic-job
就是基于quartz
二次开发之后的分布式调度解决方案。
使用 Quartz
可以很方便地与 Spring
集成,并且支持动态添加任务和集群。但是,Quartz
使用起来也比较麻烦,API 繁琐。
并且,Quzrtz
并没有内置 UI 管理控制台,不过你可以使用 quartzui (opens new window) 这个开源项目来解决这个问题。
另外,Quartz
虽然也支持分布式任务。但是,它是在数据库层面,通过数据库的锁机制做的,有非常多的弊端比如系统侵入性严重、节点负载不均衡。有点伪分布式的味道。
优缺点总结:
- 优点: 可以与
Spring
集成,并且支持动态添加任务和集群。 - 缺点 :分布式支持不友好,没有内置 UI 管理控制台、使用麻烦(相比于其他同类型框架来说)
2 Elastic-Job
优缺点总结:
- 优点 :可以与
Spring
集成、支持分布式、支持集群、性能不错 - 缺点 :依赖了额外的中间件比如 Zookeeper(复杂度增加,可靠性降低、维护成本变高)
3 XXL-Job
从上图可以看出,XXL-JOB
由 调度中心 和 执行器 两大部分组成。调度中心主要负责任务管理、执行器管理以及日志管理。执行器主要是接收调度信号并处理。另外,调度中心进行任务调度时,是通过自研 RPC 来实现的。
不同于 Elastic-Job
的去中心化设计, XXL-JOB
的这种设计也被称为中心化设计(调度中心调度多个执行器执行任务)。
和 Quzrtz
类似 XXL-JOB
也是基于数据库锁调度任务,存在性能瓶颈。不过,一般在任务量不是特别大的情况下,没有什么影响的,可以满足绝大部分公司的要求。