今天有系统对接需要使用接口推送历史数据,因为是小程序的后台,不能写一个按钮来调用方法推送历史数据,就想通过定时任务来向其他系统推送历史数据。
目前定时任务的种类有四种:
名称 | 特点 |
quartz | quartz 是一个开源组织提供的功能强大的开源项目,既能按照一个指定的时间进行简单的调度作业,也能根据一个时间间隔进行循环调度作业,还能把多个作业和多个不同的触发器进行关联,进行复杂的调度作业 |
spring task | 是一个相较于quartz轻量的定时任务框架,从spring3.0后spring就把task整合进去,如果使用的话不需要额外引入jar包,springboot项目下可以很简单的实现大部分的业务需求,并且实现起来很简单,缺点下面会涉及到 |
timer | java中自带的定时任务,缺点比多 |
scheduledExecutorService | java1.5后新增的定时任务,进行简单的任务调度,跟timer相比推荐使用这一个 |
要了解的是
1 每种定时任务支持做什么
2定时任务中的cron 的写法和意义
cron表达式详解:
cron表达式是一个控制触发调度任务时间的字符串,这个表达式中是有至少6(最多7个)个时间域,并且时间域之间用空格隔开。为什么会有至少6个时间域最多7个时间域这种说法呢?第一不同的定时允许的cron表达式不同,spring task的cron允许的如时间域就是6个,没有年的时间域,如果不为6个数量会报错(Cron expression must consist of 6),而quartz是七个时间域;第二
quartz的七个时间域中的年可以省略不写。
由左03向右时间域代表的意义:
时间域 | 含义 | 允许出现字符 | 有效范围 |
seconds | 秒 | “, - * /”四个特殊字符 | 0-59的整数 |
minutes | 分 | “, - * /”四个特殊字符 | 0-59的整数 |
hours | 小时 | “, - * /”四个特殊字符 | 0-59的整数 |
dayofMonth | 一个月中第几天 | “, - * / ? L W C”八个特殊字符 | 0-31整数 |
month | 月 | “, - * /”四个特殊字符 | 1-12或JAN-DEC |
dayofweek | 一周内第几天 | ",- * / ? L C #"八个字符 | 1-7的整数或SUN-SAT两个范围。1表示星期天,2表示星期一 |
year | 年(spring task不存在,quartz可以省略不写) | “, - * /”四个特殊字符 | 1970-2099 |
特殊字符的含义:
符号 | 意义 | 使用域 |
* | 在该时间域匹配任何值 | 所有时间域 |
, | 枚举值逗号前后的值都会执行 | 所有时间域 |
/ | 开始时间开始出发,然后每隔/后时间触发一次 | |
- | 表示一个时间范围 | 所有时间域 |
? | 不确定的值,因为月中日,和型其中日可能冲突,所以用? 0 0 0 1 * * 每天都触发 0 0 0 1 * ? 每月1号触发 | 只在dayOfMonth 和dayOfWeek使用 |
L | 表示最后,如果在星期域上使用5L表示最后一个周四触发 | 只在dayOfMonth 和dayOfWeek使用 |
W | 表示工作日,周一到周五,如果dayOfMonth写的是6w,假如6日是工作日则触发,如果6日是周六则在5日(周五触发),如果6日是周日,则7日触发(周一) | 只在dayOfMonth使用 |
LW | 表示一个月的最后一个工作日(周五) | 只在dayOfMonth使用 |
# | 用于确定每个月的第几个星期几,例如4#2 表示某月的第二个星期三 | 只在dayOfMonth使用 |
在工作中发现springboot 使用springtask 特别简单,但是发现他的时间域只有6个,不存在年的时间域,也就是意味着使用spring task的话不能指定时间触发一次这种定时任务。
quartz:
quartz有几个核心的概念:scheduler,trigger,job,jobDetail,其中trigger和job和jobDetail是元数据,scheduler为实际进行任务调度的控制器。
trigger : 触发器,用于定义调度任务的时间规则,quartz的触发有四种,,simpleTrigger,cronTriggerDailyTimeIntervalTrigger,CalendarIntervalTrigger。但是simpleTrigger和cronTrigger是最常用的。
job &jobDetail : quartz 把定时任务分成两部分,其中job是来定义任务的执行逻辑,jobDetail是用来描述job的定义(例如job接口的实现类以及其他相关的静态信息)。quartz主要有两种类型的job,stateLessJob和stateFulJob
scheduler: 实际执行逻辑调度的控制器,quartz提供了两种调度器的工厂类 DirectSchedulerFactory和StdSchedulerFactory等工厂类,用于支持scheduler相关对象的产生。StdSchedulerFactory是对org.quartz.SchedulerFactory接口的一个实现。是使用一套属性(NameValueCollection)来创建和初始化Quartz Scheduler。这些属性通常在文件中存储和加载。也可以通过编写程序来直接操作工厂。简单的调用工厂的getScheduler()就可以产生一个scheduler,初始化(以及它的ThreadPool、jobStore 和DataSources),并返回一个公共的接口。 DirectSchedulerFactory是SchedulerFactory的另一个实现,使用它不允许使用声名式配置,是使用硬编码的方式设置scheduler,所以不推荐。
下面盗用两个比较重要的时序图:
springboot2.x版本已经把quartz添加入框架,不需要单独引入jar包。但是我们项目上还没有使用这么高的springboot版本,我就需要引入一个jar包。
simpleTrigger是Trigger接口的一个实现,它可以触发一个已经安排调度程序的任务,可以触发一个固定时间的定时任务,也可以指定时间间隔重复执行该任务。simpleTrigger包含几个特点:开始时间、结束时间、重复次数以及重复执行的时间间隔。simpleTrigger 实例创建依赖于TriggerBuilder和SimpleScheduleBuilder。
下面是我的pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>job</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>job</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--必须添加,要不然会出错,项目无法启动 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<!--引入quartz相关依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
我的任务类
@Component
@EnableScheduling
public class MyJob {
/*
* 方法1
*/
public void firstJob(){
System.out.println(Thread.currentThread().getName()+"the firstJob is running,the time is"+System.currentTimeMillis());
}
/*
* 方法2
*/
public void secJob(){
System.out.println(Thread.currentThread().getName()+"the secJob is running,the time is"+System.currentTimeMillis());
}
}
先使用simpleTrigger
@Configuration
public class QuartzConfig {
//创建一个jobDetail
@Bean(name="simpleJobDetail")
public MethodInvokingJobDetailFactoryBean simpleJobDetail(MyJob myJob){
MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean();
//设置是否并发执行
jobDetail.setConcurrent(false);
//设置要执行定时任务的对象
jobDetail.setTargetObject(myJob);
//需要执行的方法
jobDetail.setTargetMethod("firstJob");
return jobDetail;
}
//创建一个simple
@Bean(name="simpleTrigger")
public SimpleTriggerFactoryBean simpleTrigger(JobDetail simpleJobDetail){
SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean();
trigger.setJobDetail(simpleJobDetail);
trigger.setStartDelay(0);
trigger.setRepeatInterval(10000);
return trigger;
}
//创建一个schedule
@Bean(name = "scheduler")
public SchedulerFactoryBean schedulerFactory(Trigger simpleTrigger){
SchedulerFactoryBean bean = new SchedulerFactoryBean();
bean.setStartupDelay(5);
bean.setTriggers(simpleTrigger);
return bean;
}
}
执行后控制台显示为
程序启动后定时任务延时5s启动,并且可以看出quartz是多线程启动的,并且默认核心线程是10个。
两种quartz的定时人任务
@Configuration
public class QuartzConfig {
/******************************simple模式开始*******************************/
//创建一个jobDetail
@Bean(name="simpleJobDetail")
public MethodInvokingJobDetailFactoryBean simpleJobDetail(MyJob myJob){
MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean();
//设置是否并发执行
jobDetail.setConcurrent(false);
//设置要执行定时任务的对象
jobDetail.setTargetObject(myJob);
//需要执行的方法
jobDetail.setTargetMethod("firstJob");
return jobDetail;
}
//创建一个simple
@Bean(name="simpleTrigger")
public SimpleTriggerFactoryBean simpleTrigger(JobDetail simpleJobDetail){
SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean();
trigger.setJobDetail(simpleJobDetail);
trigger.setStartDelay(0);
trigger.setRepeatInterval(1000);
return trigger;
}
/******************************simple模式结束************************************/
/******************************cron模式开始***********************************/
//创建一个cronJobDetail
@Bean(name="cronJobDetail")
public MethodInvokingJobDetailFactoryBean cronJobDetail(MyJob myJob){
MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean();
jobDetail.setConcurrent(false);
jobDetail.setTargetObject(myJob);
jobDetail.setTargetMethod("secJob");
return jobDetail;
}
@Bean(name="cronTrigger")
public CronTriggerFactoryBean cronTrigger(JobDetail cronJobDetail){
CronTriggerFactoryBean trigger = new CronTriggerFactoryBean();
trigger.setJobDetail(cronJobDetail);
trigger.setCronExpression("0/5 * * * * ? *");
return trigger;
}
/*******************************cron模式结束*****************************************/
//创建一个schedule
/*@Bean(name = "scheduler")
public SchedulerFactoryBean schedulerFactory(Trigger simpleTrigger){
SchedulerFactoryBean bean = new SchedulerFactoryBean();
bean.setStartupDelay(5);
bean.setTriggers(simpleTrigger);
return bean;
}*/
@Bean(name = "scheduler")
public SchedulerFactoryBean schedulerFactory(Trigger simpleTrigger,Trigger cronTrigger){
SchedulerFactoryBean bean = new SchedulerFactoryBean();
bean.setStartupDelay(5); //如果不设置,扫描到这个配置就会触发定时任务
bean.setTriggers(simpleTrigger,cronTrigger);
return bean;
}
}
当把另个jobDetail 都设置为不并发执行时jobDetail.setConcurrent(false);,两个定时任务不会并发执行,定时任务会等待另一定时任务执行结束才会执行。如下:
spring task:
使用spring task 很简单,只需要在启动类上加上 @EnableScheduling 开始定时任务,然后新建任务类,并且能够让spring扫描到。
@SpringBootApplication
@EnableScheduling
public class JobApplication {
public static void main(String[] args) {
SpringApplication.run(JobApplication.class, args);
}
}
/*
*spring task 定时任务demo
*/
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class TaskDemo {
@Scheduled(cron="0/5 * * * * *")
public void testScheduler1(){
System.out.println(Thread.currentThread().getName()+"正在执行任务的名字是testScheduler1");
}
@Scheduled(cron="0/5 * * * * *")
public void testScheduler2(){
System.out.println(Thread.currentThread().getName()+"正在执行任务的名字是testScheduler2");
}
}
上面是一个最简单spring task的定时任务,可以看出两个定时任务会走同一个线程。如果定时任务少影响不大,但是如果有一个定时任务执行过程很长,会影响下面线程的执行。
public class TaskDemo {
@Scheduled(cron="0/5 * * * * *")
public void testScheduler1(){
System.out.println(Thread.currentThread().getName()+"正在执行任务的名字是testScheduler1,执行时间是"+System.currentTimeMillis());
try {
Thread.currentThread().sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Scheduled(cron="0/5 * * * * *")
public void testScheduler2(){
System.out.println(Thread.currentThread().getName()+"正在执行任务的名字是testScheduler2,执行时间是"+System.currentTimeMillis());
}
}
线程会一直等待线程的锁被释放后才会继续执行。
@Component
public class TaskDemo2 {
// @Scheduled(cron="0/5 * * * * *")
public void task1(){
System.out.println(Thread.currentThread().getName()+"正在执行任务***cron****的名字是task1,执行时间是"+System.currentTimeMillis());
}
@Scheduled(fixedRate = 5000)
public void task2(){
System.out.println(Thread.currentThread().getName()+"正在执行任务***fixedRate****的名字是task2,执行时间是"+System.currentTimeMillis());
}
//@Scheduled(fixedDelay = 5000)
public void task3(){
System.out.println(Thread.currentThread().getName()+"正在执行任务***fixedDelay****的名字是task3,执行时间是"+System.currentTimeMillis());
}
单独执行 fixedRate和fixedDelay的执行结果是:
fixedRate:是按照一个固定的时间间隔进行执行的,任务执行时间间隔是从上一次方法开始执行算起,如果上一次方法阻塞了,下一次也不会执行,但是在阻塞这段时间内累计应执行的次数,当不再阻塞时,全部执行,而后再按照固定频率执行。
fixedDelay:间隔是前次任务的结束与下次任务的开始之间的时间间隔,如果上一次方法阻塞住了,那么直到上一次执行完,并过完指定的时间间隔才会执行下一次。
在实际生产中,定时任务比较多的话,单线程的方式肯定是不行的。要使用线程池来使用定时任务。
###############线程池的配置###############
thread:
corePoolSize: 10 #核心线程数
maxPoolSize: 200 #最大线程数
queueCapacity: 10 #队列容量
corePoolSize :核心线程数会一直存活,即使没有任务需要执行。当线程数小于核心线程数时,即使有空闲线程,线程池也会优先新线程处理,设置allowCoreThreadTimeout=true(默认为false)时,核心线程会超时关闭
queueCapacity:队列任务容量(阻塞队列),当核心线程达到最大时,新任务会放在队列中排队等待执行
maxPoolSize:最大线程数。当线程数>corePoolSize且任务队列已满时。线程池会创建新的线程处理任务,当线程数=maxPoolSize且任务队列已满,线程池会拒绝任务而抛出异常
/*
* 线程池的配置文件
*/
@Configuration
@EnableAsync
public class AnsyConfig {
Logger logger = LoggerFactory.getLogger(AnsyConfig.class);
@Value("${thread.corePoolSize}")
private int corePoolSize;
@Value("${thread.maxPoolSize}")
private int maxPoolSize;
@Value("${thread.queueCapacity}")
private int queueCapacity;
@Bean
public Executor taskExecutor(){
ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.initialize();
return executor;
}
}
@Component
@Async //新增注解
public class TaskDemo2 {
@Scheduled(cron="0/5 * * * * *")
public void task1(){
System.out.println(Thread.currentThread().getName()+"正在执行任务***cron***********的名字是task1,执行时间是"+System.currentTimeMillis());
}
@Scheduled(fixedRate = 5000)
public void task2(){
System.out.println(Thread.currentThread().getName()+"正在执行任务***fixedRate******的名字是task2,执行时间是"+System.currentTimeMillis());
}
@Scheduled(fixedDelay = 5000)
public void task3(){
System.out.println(Thread.currentThread().getName()+"正在执行任务***fixedDelay****的名字是task3,执行时间是"+System.currentTimeMillis());
}
}