《从零打造项目》系列文章
工具
ORM框架选型
数据库变更管理
定时任务框架
- Java定时任务技术分析
缓存
- 待更新
安全框架
- 待更新
开发规范
- 待更新
常见的业务场景:
- 某博客平台,支持定时发送文章。
- 某学习平台,定时发送学习任务通知用户
- 定时进行数据抓取等等
在项目中要求我们在某个时刻去做某件事情,下面我们就来看看有哪些方法可以实现定时任务。
JDK内置类
Timer
java.util.Timer
是 JDK 1.3 开始就已经支持的一种定时任务的实现方式。
Timer
内部使用一个叫做 TaskQueue
的类存放定时任务,它是一个基于最小堆实现的优先级队列。TaskQueue
会按照任务距离下一次执行时间的大小将任务排序,保证在堆顶的任务最先执行。这样在需要执行任务时,每次只需要取出堆顶的任务运行即可!由于某个任务的执行时间可能较长,则后面的任务运行的时间会被延迟,所以执行的时间和你预期的时间可能不一致。延迟的任务具体开始的时间,就是依据前面任务的结束时间。
核心方法:
//启动任务之后,延迟多久时间执行
void schedule(TimerTask task, long delay);
//在指定的时间执行任务
void schedule(TimerTask task, Date time);
//启动任务后,延迟多久时间执行,执行之后指定间隔多久重复执行任务
void schedule(TimerTask task, long delay, long period);
//指定时间启动任务,执行后间隔指定时间重复执行任务
void schedule(TimerTask task, Date firstTime, long period);
代码案例:
public class TimerUse {
public static void main(String[] args) {
System.out.println("当前时间: " + new Date() + "n" +
"线程名称: " + Thread.currentThread().getName());
testTimer1();
// testTimer2();
// testTimer3();
// testTimer4();
}
// 方法一:设定指定任务task在指定时间time执行 schedule(TimerTask task, long delay)
public static void testTimer1() {
Timer timer = new Timer("Timer");
timer.schedule(new TimerTask() {
public void run() {
System.out.println("当前时间: " + new Date() + "n" +
"线程名称: " + Thread.currentThread().getName());
}
}, 3500);
// 设定指定的时间time为3500毫秒
}
/**
* 方法二:设定指定任务task在指定延迟delay后间隔指定时间peroid执行 schedule(TimerTask task, long delay, long period)
*/
public static void testTimer2() {
Timer timer = new Timer("Timer");
timer.schedule(new TimerTask() {
public void run() {
System.out.println("当前时间: " + new Date() + "n" +
"线程名称: " + Thread.currentThread().getName());
}
}, 2000, 3500);
}
/**
* 方法三:在指定的时间执行任务 schedule(TimerTask task, Date time)
*/
public static void testTimer3() {
Date date = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.MINUTE, 1); // 往后推一分钟
Date time = calendar.getTime(); //获取当前系统时间
Timer timer = new Timer("Timer");
timer.schedule(new TimerTask() {
public void run() {
System.out.println("当前时间: " + new Date() + "n" +
"线程名称: " + Thread.currentThread().getName());
}
}, time);
}
/**
* 方法四:安排指定的任务task在指定的时间firstTime开始进行重复的固定速率period执行. schedule(TimerTask task, Date firstTime,
* long period)
*/
public static void testTimer4() {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, 12); // 控制小时
calendar.set(Calendar.MINUTE, 0); // 控制分钟
calendar.set(Calendar.SECOND, 0); // 控制秒
Date time = calendar.getTime(); //获取当前系统时间
Timer timer = new Timer("Timer");
timer.schedule(new TimerTask() {
public void run() {
System.out.println("当前时间: " + new Date() + "n" +
"线程名称: " + Thread.currentThread().getName());
}
}, time, 1000 * 60 * 60 * 24);// 这里设定将延时每天固定执行
}
}
注意事项
1、创建一个 Timer 对象相当于新启动了一个线程,但是这个新启动的线程,并不是守护线程。它一直在后台运行,通过如下代码将新启动的 Timer 线程设置为守护线程。
Timer timer = new Timer(true);
变为守护线程,则意味着主线程执行结束,则程序就结束了,定时任务也就不会执行。
2、当计划时间早于当前时间,则任务立即被运行。
ScheduledExecutorService
ScheduledExecutorService
是一个接口,有多个实现类,比较常用的是 ScheduledThreadPoolExecutor
。
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor implements ScheduledExecutorService {}
ScheduledThreadPoolExecutor
的状态管理、入队操作、拒绝操作等都是继承于 ThreadPoolExecutor
;ScheduledThreadPoolExecutor
主要是提供了周期任务和延迟任务相关的操作;
schedule(Runnable command, long delay, TimeUnit unit) // 无返回值的延迟任务
schedule(Callable callable, long delay, TimeUnit unit) // 有返回值的延迟任务
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) // 固定频率周期任务
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) // 固定延迟周期任务
代码示例:
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(1);
long delay = 1L;
long period = 2L;
// 延迟1s,周期2s
executor.scheduleAtFixedRate(repeatedTask, delay, period, TimeUnit.SECONDS);
// 定时任务重复执行3个周期
Thread.sleep((delay + period * 3) * 1000);
executor.shutdown();
执行结果为:
当前时间: Wed Nov 23 09:52:35 CST 2022n线程名称: main
当前时间: Wed Nov 23 09:52:36 CST 2022n线程名称: pool-1-thread-1
当前时间: Wed Nov 23 09:52:38 CST 2022n线程名称: pool-1-thread-1
当前时间: Wed Nov 23 09:52:40 CST 2022n线程名称: pool-1-thread-1
当前时间: Wed Nov 23 09:52:42 CST 2022n线程名称: pool-1-thread-1
注意事项
1、scheduleAtFixedRate
和 scheduleWithFixedDelay
是我们最常用的两个方法,两者略有区别,前者为固定频率周期任务,如果任务执行时间超出周期时,下一次任务会立刻运行;后者为固定延迟周期任务,无论执行时间是多少,其结果都是在执行完毕后,停顿固定的时间,然后执行下一次任务。
2、ScheduledThreadPoolExecutor 线程最多为核心线程,最大线程数不起作用,因为 DelayedWorkQueue 是无界队列。
更多内容推荐阅读:并发系列(7)之 ScheduledThreadPoolExecutor 详解
小结
在 JDK 中,内置了两个类,可以实现定时任务的功能:
java.util.Timer
:可以通过创建java.util.TimerTask
调度任务,在同一个线程中串行执行,相互影响。也就是说,对于同一个 Timer 里的多个 TimerTask 任务,如果一个 TimerTask 任务在执行中,其它 TimerTask 即使到达执行的时间,也只能排队等待。因为 Timer 是串行的,同时存在 坑坑 ,所以后来 JDK 又推出了 ScheduledExecutorService ,Timer 也基本不再使用。java.util.concurrent.ScheduledExecutorService
:在 JDK 1.5 新增,基于线程池设计的定时任务类,每个调度任务都会被分配到线程池中并发执行,互不影响。这样,ScheduledExecutorService 就解决了 Timer 串行的问题。
在日常开发中,我们很少直接使用 Timer 或 ScheduledExecutorService 来实现定时任务的需求。主要有几点原因:
- 它们仅支持按照指定频率,不直接支持指定时间的定时调度,需要我们结合 Calendar 自行计算,才能实现复杂时间的调度。例如说,每天、每周五、2019-11-11 等等,不支持 Cron 表达式。
- 它们是进程级别,而我们为了实现定时任务的高可用,需要部署多个进程。此时需要等多考虑,多个进程下,同一个任务在相同时刻,不能重复执行。
- 项目可能存在定时任务较多,需要统一的管理,此时不得不进行二次封装。
所以,一般情况下,我们会选择专业的调度任务中间件。
中间件
Spring Task
由于 SpringTask 已经存在于 Spring 框架中,所以无需添加依赖。
下面我们弄个小 Demo 测试一下,新建一个项目,引入依赖。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
</dependencies>
添加 SpringTask 的配置类
@Configuration
@EnableScheduling
public class SpringTaskConfig {
}
在 application.yml 添加关于 Spring Task 的配置,如下:
spring:
task:
# Spring Task 调度任务的配置,对应 TaskSchedulingProperties 配置类
scheduling:
thread-name-prefix: job- # 线程池的线程名的前缀。默认为 scheduling- ,建议根据自己应用来设置
pool:
size: 10 # 线程池大小。默认为 1 ,根据自己应用来设置
shutdown:
await-termination: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true
await-termination-period: 60 # 等待任务完成的最大时长,单位为秒。默认为 0 ,根据自己应用来设置
spring.task.scheduling.shutdown
配置项,是为了实现 Spring Task 定时任务的优雅关闭。
定时任务测试类
@Service
@Slf4j
public class ScheduledTaskService {
private final AtomicInteger counts = new AtomicInteger();
// @Scheduled(cron = "0 0/10 * ? * ?")//每10分钟执行一次
@Scheduled(fixedRate = 3000) // 每 3秒执行一次
public void pushMessage() {
log.info("[execute]定时第({})给用户发送通知", counts.incrementAndGet());
}
}
最后创建一个启动类,启动项目,控制台输出如下:
Spring Task 支持 Cron 表达式 。Cron 表达式主要用于定时作业(定时任务)系统定义执行时间或执行频率的表达式,非常厉害,你可以通过 Cron 表达式进行设置定时任务每天或者每个月什么时候执行等等操作。
Cron 格式中每个时间元素的说明
平时可以找一个 Cron 表达式生成器在线网站按需生成想要的表达式。
SpringTask 功能小结:
1、SpringTask 内置于 Spring 框架,相比于Quartz更加简单方便,不需要引入其他依赖。
2、Spring Task 底层是基于 JDK 的 ScheduledThreadPoolExecutor
线程池来实现的。
3、支持 Cron 表达式
4、只支持单机,功能单一
Quartz
Github:https://github.com/quartz-scheduler/quartz
Quartz 作为一个优秀的开源调度框架,Quartz 具有以下特点:
- 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
- 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
- 分布式和集群能力,Terracotta 收购后在原来功能基础上作了进一步提升。
另外,作为 Spring 默认的调度框架,Quartz 很容易与 Spring 集成实现灵活可配置的调度功能。
在 Quartz 体系结构中,有三个组件非常重要:
- Scheduler :调度器。Scheduler启动Trigger去执行Job。
- Trigger :触发器。用来定义 Job(任务)触发条件、触发时间,触发间隔,终止时间等。四大类型:SimpleTrigger(简单的触发器)、CornTrigger(Cron表达式触发器)、DateIntervalTrigger(日期触发器)、CalendarIntervalTrigger(日历触发器)。
- Job :任务。具体要执行的业务逻辑,比如:发送短信、发送邮件、访问数据库、同步数据等。
Quartz 应用分为单机模式和集群模式。实际应用中,我们都会选择集群模式,关于 Quartz 的使用,后续会单独出一篇文章进行介绍。
Quartz 框架出现的比较早,后续不少定时框架,或多或少都基于 Quartz 研发的,比如当当网的elastic-job
就是基于quartz
二次开发之后的分布式调度解决方案。
并且,Quartz
并没有内置 UI 管理控制台,不过你可以使用 quartzui 这个开源项目来解决这个问题。
Quartz
虽然也支持分布式任务。但是,它是在数据库层面,通过数据库的锁机制做的,有非常多的弊端比如系统侵入性严重、节点负载不均衡。
Quartz 优缺点:
- 可以与
Spring
集成,并且支持动态添加任务和集群。 - 分布式支持不友好,没有内置 UI 管理控制台、使用麻烦(相较于其他框架)。
XXL-JOB
官方说明:XXL-JOB 是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
通俗来讲:XXL-JOB 是一个任务调度框架,通过引入 XXL-JOB 相关的依赖,按照相关格式撰写代码后,可在其可视化界面进行任务的启动,执行,中止以及包含了日志记录与查询和任务状态监控。
官方文档:https://www.xuxueli.com/xxl-job/
GitHub:https://github.com/xuxueli/xxl-job
Gitee:http://gitee.com/xuxueli0323/xxl-job
特性:
Xxl-job 解决了很多 Quartz 的不足。
XXL-JOB
的架构设计如下图所示:
从上图可以看出,XXL-JOB
由 调度中心 和 执行器 两大部分组成。调度中心主要负责任务管理、执行器管理以及日志管理。执行器主要是接收调度信号并处理。另外,调度中心进行任务调度时,是通过自研 RPC 来实现的。
关于 xxl-job 的使用,下篇文章会详细介绍的。
xxl-job 的优点相对于 Quartz 非常明显,使用更加简单,而且内置了 UI 管理控制台。
Elastic-Job
Github:https://github.com/apache/shardingsphere-elasticjob
官方文档:https://shardingsphere.apache.org/elasticjob/current/cn/overview/
ElasticJob 是面向互联网生态和海量任务的分布式调度解决方案,基于Quartz
和ZooKeeper
开发,由两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。 它通过弹性调度、资源管控、以及作业治理的功能,打造一个适用于互联网场景的分布式调度解决方案,并通过开放的架构设计,提供多元化的作业生态。 它的各个产品使用统一的作业 API,开发者仅需一次开发,即可随意部署。
ElasticJob-Lite 的架构设计如下图所示:
从上图可以看出,Elastic-Job
没有调度中心这一概念,而是使用 ZooKeeper
作为注册中心,注册中心负责协调分配任务到不同的节点上。
Elastic-Job 中的定时调度都是由执行器自行触发,这种设计也被称为去中心化设计(调度和处理都是执行器单独完成)。
@Component
@ElasticJobConf(name = "dayJob", cron = "0/10 * * * * ?", shardingTotalCount = 2,
shardingItemParameters = "0=AAAA,1=BBBB", description = "简单任务", failover = true)
public class TestJob implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
log.info("TestJob任务名:【{}】, 片数:【{}】, param=【{}】", shardingContext.getJobName(), shardingContext.getShardingTotalCount(),
shardingContext.getShardingParameter());
}
}
Elastic-Job 支持的功能:
关于 Elastic-Job 的使用,未来会抽时间出一篇文章的。
Elastic-Job 相较于 XXL-JOB 缺点也比较明显,就是需要引入额外的中间件,比如 Zookeeper,增加了操作难度。
总结
由于本人目前接触到的框架有限,除了上述四种中间件,还有不少大公司自研的中间件,如果从个人开发的角度来看,推荐大家使用 XXL-JOB,开箱即用,且配有可视化界面。如果想对其他中间件有所了解,这里推荐阅读如下两篇文章: