定时任务
# 1. 什么时候会用到定时任务?
- 某系统凌晨要进行数据备份。
- 某电商平台,用户下单半个小时未支付的情况下需要自动取消订单。
- 某媒体聚合平台,每 10 分钟动态抓取某某网站的数据为自己所用。
- 某博客平台,支持定时发送文章。
- 某基金平台,每晚定时计算用户当日收益情况并推送给用户最新的数据。
- ......
这些场景往往都要求我们在某个特定的时间去做某个事情
# 2.单机定时任务选型
# 自写代码
使用while+sleep
可以让具体执行相关逻辑的线程进行休眠指定的时间,就可以完成在指定时间后完成相应的任务
缺点:
- 自己实现,难免会出现bug
- 无法使用cron表达时,无法指定周期执行,只能执行一次时间滞后任务
- 浪费线程资源
# Timer
Timer是jdk自带的以中国定时任务实现方式
原理:Timer
内部使用一个叫做 TaskQueue
的类存放定时任务,它是一个基于最小堆实现的优先级队列。TaskQueue
会按照任务距离下一次执行时间的大小将任务排序,保证在堆顶的任务最先执行。这样在需要执行任务时,每次只需要取出堆顶的任务运行即可!
# 实现案例
// 示例代码:
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
缺点:
- 不支持cron表达式
- 一个
Timer
一个线程,这就导致Timer
的任务的执行只能串行执行,一个任务执行时间过长的话会影响其他任务(性能非常差),再比如发生异常时任务直接停止(Timer
只捕获了InterruptedException
)。
# SechduledExecutorService
ScheduledExecutorService
是一个接口,有多个实现类,比较常用的是 ScheduledThreadPoolExecutor
。
ScheduledThreadPoolExecutor
本身就是一个线程池,支持任务并发执行。并且,其内部使用 DelayQueue
作为任务队列。
// 示例代码:
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
优缺点:
- 解决了timer中线程问题
- 无法使用cron表达式
# spring中的@Scheduled
/**
* cron:使用Cron表达式。 每分钟的1,2秒运行
*/
@Scheduled(cron = "1-2 * * * * ? ")
public void reportCurrentTimeWithCronExpression() {
log.info("Cron Expression: The time is now {}", dateFormat.format(new Date()));
}
2
3
4
5
6
7
8
注意:
- 需要在启动类上加上@EnableScheduled注解
- 底层使用的是SechduledExecutorService,但是默认只有一个线程,也即是所有的定时任务都由同一个线程执行,如果一个任务占用的时间过久,会影响下一个任务的执行时间。
那么怎么自定义线程池,让执行定时任务的线程有自己的名字和线程数量呢?
@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
private final int POOL_SIZE = 10;
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(POOL_SIZE);
threadPoolTaskScheduler.setThreadNamePrefix("my-scheduled-task-pool-");
threadPoolTaskScheduler.initialize();
scheduledTaskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如上面所示,我们需要创建一个配置类,并是实现SchedulingConfigurer 重写对应的方法就可以了。
优缺点:
可以使用cron了,是一大优点
但是功能比较单一
# 时间轮
Kafka、Dubbo、ZooKeeper、Netty 、Caffeine 、Akka 中都有对时间轮的实现。
https://javaguide.cn/system-design/schedule-task.html#spring-task
# 3. 分布式定时任务
分布式环境下单机定时任务的问题:
①如何保证集群部署得机器上定时任务不被重复执行
②如何动态调整定时任务的执行时间(不重启服务)
③部署定时任务的机器发生故障时,如何进行故障转移
④如何对集群中哪些进行了定时任务进行监控
⑤单机性能出现瓶颈,如何进行扩展
# XXL-JOB
一款优秀的轻量级分布式任务调度框架,支持任务可视化管理、弹性扩容缩容、任务失败重试和告警、任务分片等功能
解决了很多quartz的不足之处
架构设计:
从上图可以看出,XXL-JOB
由 调度中心 和 执行器 两大部分组成。调度中心主要负责任务管理、执行器管理以及日志管理。执行器主要是接收调度信号并处理。另外,调度中心进行任务调度时,是通过自研 RPC 来实现的。
不同于 Elastic-Job
的去中心化设计, XXL-JOB
的这种设计也被称为中心化设计(调度中心调度多个执行器执行任务)。
和 Quzrtz
类似 XXL-JOB
也是基于数据库锁调度任务,存在性能瓶颈。不过,一般在任务量不是特别大的情况下,没有什么影响的,可以满足绝大部分公司的要求。
不要被 XXL-JOB
的架构图给吓着了,实际上,我们要用 XXL-JOB
的话,只需要重写 IJobHandler
自定义任务执行逻辑就可以了,非常易用!
@JobHandler(value="myApiJobHandler")
@Component
public class MyApiJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String param) throws Exception {
//......
return ReturnT.SUCCESS;
}
}
2
3
4
5
6
7
8
9
10
11
还可以使用注解的方式:
@XxlJob("myAnnotationJobHandler")
public ReturnT<String> myAnnotationJobHandler(String param) throws Exception {
//......
return ReturnT.SUCCESS;
}
2
3
4
5
6
优缺点总结:
- 优点:开箱即用(学习成本比较低)、与 Spring 集成、支持分布式、支持集群、内置了 UI 管理控制台。
- 缺点:不支持动态添加任务(如果一定想要动态创建任务也是支持的,参见:xxl-job issue277open in new window (opens new window))