定时任务
Quartz
1. Quartz Scheduler
主要设计以下元素:
- scheduler:任务调度器;
- trigger:触发器,用于定义任务调度的时间规划;
- job:被调度的任务;
- misfire:错过的,指本来应该执行,但实际没有被执行的任务调度。
其中trigger和job是任务调度的元数据,scheduler是实际执行调度的控制器。
- trigger 是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz 中主要提供了四种类型的 trigger: SimpleTrigger,CronTirgger,DateIntervalTrigger,和 NthIncludedDayTrigger。
- job:无状态的(stateless)和有状态的(stateful)。对于同一个 trigger 来 说,有状态的 job 不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job 主要有两种属性:volatility 和 durability,其中 volatility 表示任务是否被持久化到数据库存储,而 durability 表示在没有 trigger 关联的时候任务是否被保留。两者都是在值为 true 的时候任务被持久化或保留。
- 一个 job 可以被多个 trigger 关联,但是一个 trigger 只能关联一个 job。
- scheduler 由 scheduler 工厂创建:DirectSchedulerFactory 或者 StdSchedulerFactory。 一般使用 StdSchedulerFactory 工厂 较多。 Scheduler 主要有三种:RemoteMBeanScheduler, RemoteScheduler 和 StdScheduler。最常用的为 StdScheduler。
2. Quartz 的线程
- 在 Quartz 中,有两类线程,Scheduler 调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。
- Scheduler 调度线程主要有两个: 执行常规调度的线程,和执行 misfired trigger 的线程。常规调度线程轮询存储的所有 trigger, 如果有需要触发的 trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该 trigger 关联的任 务。Misfire 线程是扫描所有的 trigger,查看是否有 misfired trigger,如果有的话根据 misfire 的策略分别处理。
3. 数据存储
- Quartz 中的 trigger 和 job 需要存储下来才能被使用。Quartz 中有两种存储方式:RAMJobStore, JobStoreSupport,其中RAMJobStore 是将 trigger 和 job 存储在内存中, 而 JobStoreSupport 是基于 jdbc 将 trigger 和 job 存储到数据库中。RAMJobStore 的存取速度非常快, 但是由于其在系统被停止后所有的数据都会丢失, 所以在通常应用中, 都是使用JobStoreSupport。
- 在 Quartz 中,JobStoreSupport 使用一个驱动代理来操作 trigger 和 job 的数据存储:StdJDBCDelegate。StdJDBCDelegate 实现了大部分基于标准 JDBC 的功能接口,但是对于各种数据库来说,需要根据其具体实现的特点做某些特殊处理,因此各种数据库需要扩展 StdJDBCDelegate 以实现这些特殊处理。Quartz 已经自带了一些数据库的扩展实现,可以直接使用。
4. 使用方法
4.1 引入依赖
- 这里springboot使用的是2.3.1.RELEASE
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
4.2 创建目标任务job
- 使用实现 org.quartz.Job 接口的方式创建任务BusinessJob
package com.example.demo.job;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
@Slf4j
public class BusinessJob implements Job {
int i = 0;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDataMap dataMap = jobExecutionContext.getJobDetail().getJobDataMap();
String time = dataMap.get("time").toString();
business(time);
}
public void business(String time) {
i++;
log.info("time:{},threadName:{},i:{}", time, Thread.currentThread().getName(), i);
}
}
- 普通任务类
package com.example.demo.job;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class JobService {
int i = 0;
public void business() {
i++;
log.info("定时任务,业务方法执行,thread:{},i:{}", Thread.currentThread().getName(), i);
}
}
4.3 创建配置类QuartzConfig
- cron表达式可以在https://cron.qqe2.com/根据需要生成
package com.example.demo.config;
import com.example.demo.job.BusinessJob;
import com.example.demo.job.JobService;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Trigger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.*;
import java.time.LocalDateTime;
@Configuration
public class QuartzConfig {
//配置任务,多例的业务bean,耦合业务类,需要实现Job接口
@Bean(name = "businessJobDetail")
public JobDetailFactoryBean businessJobDetail() {
LocalDateTime localDateTime = LocalDateTime.now();
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
jobDetailFactoryBean.setJobClass(BusinessJob.class);
//将参数传给job
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("time", localDateTime);
jobDetailFactoryBean.setJobDataAsMap(jobDataMap);
return jobDetailFactoryBean;
}
//配置任务,单例的业务bean
@Bean(name = "jobServiceBeanDetail")
public MethodInvokingJobDetailFactoryBean jobServiceBeanDetail(JobService jobService) {
MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean();
//是否并发执行
jobDetail.setConcurrent(false);
//需要执行的实体bean
jobDetail.setTargetObject(jobService);
//需要执行的方法
jobDetail.setTargetMethod("business");
return jobDetail;
}
//配置简单触发器
@Bean(name = "simpleTrigger")
public SimpleTriggerFactoryBean simpleTrigger(JobDetail businessJobDetail) {//businessJobDetail
SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean();
trigger.setJobDetail(businessJobDetail);
//设置启动延迟
trigger.setStartDelay(0);
//每隔5s执行一次
trigger.setRepeatInterval(5000);
return trigger;
}
//配置cron触发器
@Bean(name = "cronTrigger")
public CronTriggerFactoryBean cronTrigger(JobDetail jobServiceBeanDetail) {//目标任务jobServiceBeanDetail
CronTriggerFactoryBean triggerFactoryBean = new CronTriggerFactoryBean();
triggerFactoryBean.setJobDetail(jobServiceBeanDetail);
//每隔6s执行一次
triggerFactoryBean.setCronExpression("0/6 * * * * ?");
return triggerFactoryBean;
}
//配置调用工厂,将所有的触发器引入
@Bean(name = "scheduler")
public SchedulerFactoryBean schedulerFactory(Trigger cronTrigger, Trigger simpleTrigger) {//需要管理的触发器cronTrigger,simpleTrigger
SchedulerFactoryBean bean = new SchedulerFactoryBean();
//延迟1s启动
bean.setStartupDelay(1);
//注册触发器,可以注册多个
bean.setTriggers(cronTrigger, simpleTrigger);
return bean;
}
}
4.3 查看打印
spring task
- Spring 从 3.0 开始增加了自己的任务调度器,它是通过扩展 java.util.concurrent 包下面的类来实现的。使用 spring task 非常简单,只需要给定时任务类添加@Component 和 @EnableScheduling注解,给任务方法添加@Scheduled注解,并让 Spring 扫描到该类即可。
1. 创建任务TaskService
package com.example.demo.schedule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@EnableScheduling
public class TaskService {
//每隔1s执行一次
@Scheduled(fixedRate = 1000)
public void fixMethod() {
try {
log.info("fixMethod,thread:{}",Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//前一个任务执行完5s后执行
@Scheduled(fixedDelay = 5000)
public void delayMethod() {
log.info("delayMethod,thread:{}",Thread.currentThread().getName());
}
//每隔10s执行一次
@Scheduled(cron = "0/10 * * * * ?")
public void cronMethod() {
log.info("cronMethod,thread:{}",Thread.currentThread().getName());
}
}
- 启动项目,查看打印
- fixMethod方法没有每隔1s执行一次;
- delayMethod方法没有在前一个delayMethod方法执行后5s执行;
- cronMethod方法也没有每隔10s执行一次;
- 通过打印可以看出定时任务只有scheduling-1一个线程在执行,属于单线程,fixMethod方法执行时间是2s,即使设置时间间隔是1s,后一次执行也要等到前一次代码执行完才能执行,属于同步执行。
2. 多线程配置
- 当前项目中存在多个任务时,可以配置 executor 线程池,这里 executor 的含义和 java.util.concurrent.Executor 是一样的,pool-size 的大小官方推荐为 5~10。
- 创建配置类ScheduleConfig,实现SchedulingConfigurer接口
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
//配置线程池--触发器和任务共用
@Bean
public Executor taskExecutor(){
return Executors.newScheduledThreadPool(10);
}
}
- 查看打印
- 线程池内有10个线程,即使任务多了也不影响线程之间的执行时间间隔。
3. 异步
- 配置类中添加注解@EnableAsync
- 任务类在方法上添加注解@Async
- 查看打印