Java定时任务在实际开发中还是用到很多的,像刷新大屏可视化数据、电商下单付款计时、发送邮件等。
实现方法大致可以分为两大类吧,
java.util.Timer
是 JDK 1.3 开始就已经支持的一种定时任务的实现方式。
Timer
内部使用一个叫做 TaskQueue
的类存放定时任务,它是一个基于最小堆实现的优先级队列。TaskQueue
会按照任务距离下一次执行时间的大小将任务排序,保证在堆顶的任务最先执行。这样在需要执行任务时,每次只需要取出堆顶的任务运行即可!
Timer
和TimerTask
用于在后台线程中调度任务的java.util
类。TimerTask
负责任务的执行,Timer
负责任务的调度。
Timer
提供了三种定时模式:
fixed delay
)fixed rate
)//在当前时间往后delay个毫秒开始执行
public void schedule(TimerTask task, long delay) {...}
//在指定的time时间点执行
public void schedule(TimerTask task, Date time) {...}
public static void main(String[] args) {
//定义一个Timer
Timer timer = new Timer("test-timer");
//定义一个TimerTask
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("任务执行时间:" + new Date() + "------------"
+ "线程:" + Thread.currentThread().getName());
}
};
long delay = 3000L;
timer.schedule(task, delay);
System.out.println("任务添加时间:" + new Date() + "------------"
+ "线程:" + Thread.currentThread().getName());
}
工作方式:当达到我们指定的时间,执行一次结束
任务虽然运行结束,但进程没有被销毁。并且执行任务的线程名为我们定义的Timer
的名称。我们看一下源码:
public class Timer {
//小顶堆,用来存放timeTask
private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);
public Timer(String name) {
thread.setName(name);
thread.start();
}
}
public abstract class TimerTask implements Runnable {
long nextExecutionTime;
long period = 0;
public abstract void run();
}
TaskQueue
:基于小顶堆实现,用来存放timerTask
TimerThread
:任务执行线程,继承Thread
类nextExecutionTime
:假如任务需要多次执行表示下一次执行时间period
:每次任务执行间隔时间run()
:我们执行任务的内容创建一个 Timer
对象就是新启动了一个线程,但是这个新启动的线程,并不是守护线程,它一直在后台运行,通过如下 可以将新启动的 Timer
线程设置为守护线程。我们可以使用以下构造方法(public Timer(boolean isDaemon)
或public Timer(String name, boolean isDaemon)
)来设置。
Fixed Delay
模式(固定间隔)//从当前时间开始delay个毫秒数开始定期执行,周期是period个毫秒数
public void schedule(TimerTask task, long delay, long period) {...}
//从指定的firstTime开始定期执行,往后每次执行的周期是period个毫秒数
public void schedule(TimerTask task, Date firstTime, long period){...}
public static void main(String[] args) {
Timer timer = new Timer("test-timer");
MyTimerTask task1 = new MyTimerTask("任务1");
MyTimerTask task2 = new MyTimerTask("任务2");
long delay = 1000L;
long period = 2000L;
timer.schedule(task1, delay, period);
timer.schedule(task2, new Date(), period);
}
static class MyTimerTask extends TimerTask {
private String taskName;
public MyTimerTask(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(taskName + "执行时间:" + new Date() + "------------"
+ "线程:" + Thread.currentThread().getName());
}
}
工作方式:
TimerThread
没有执行其他任务),如有其他任务在执行,那就需要等到其他任务执行完成才能执行period
时间。根据任务运行结果来看,任务1和任务2并没有按照我们所预期的间隔2秒来执行,基本上间隔都是在6秒。而且我们注册在同一Timer
的任务,都是使用同一个在同一个线程上执行。TimerTask
是以队列的方式一个一个被顺序运行的,所以执行的时间和预期的时间可能不一致,因为前面的任务可能消耗的时间较长,则后面的任务运行的时间会被延迟。延迟的任务具体开始的时间,就是依据前面任务的"结束时间" 。
Fixed Rate
模式(固定速率)//从当前时间开始delay个毫秒数开始定期执行,周期是period个毫秒数
public void scheduleAtFixedRate(TimerTask task, long delay, long period) {...}
//从指定的firstTime开始定期执行,往后每次执行的周期是period个毫秒数
public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period){...}
public static void main(String[] args) {
Timer timer = new Timer("test-timer");
MyTimerTask task1 = new MyTimerTask("任务1");
MyTimerTask task2 = new MyTimerTask("任务2");
long delay = 1000L;
long period = 5000L;
timer.scheduleAtFixedRate(task1, delay, period);
timer.scheduleAtFixedRate(task2, new Date(System.currentTimeMillis() - 1000L), period);
}
static class MyTimerTask extends TimerTask {
private String taskName;
public MyTimerTask(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(taskName + "执行时间:" + new Date() + "------------"
+ "线程:" + Thread.currentThread().getName());
}
}
工作方式:
一般情况下和schedule()
方法没有什么区别,我们可以观察结果发现任务2第一次和第二次执行相差4秒,我们设置开始时间为当前时间前1秒,scheduleAtFixedRate()
当计划时间早于当前时间,则任务立即被运行。
通过上面分析,Java的定时调度可以通过Timer&TimerTask
来实现。由于其实现的方式为单线程,因此从JDK1.3发布之后就一直存在一些问题,大致如下:
ScheduledExecutorService
在设计之初就是为了解决Timer&TimerTask
的这些问题。因为天生就是基于多线程机制,所以任务之间不会相互影响(只要线程数足够。当线程数不足时,有些任务会复用同一个线程)。
除此之外,因为其内部使用的延迟队列,本身就是基于等待/唤醒
机制实现的,所以CPU并不会一直繁忙。同时,多线程带来的CPU资源复用也能极大地提升性能。
因为ScheduledExecutorService
继承于ExecutorService
,所以本身支持线程池的所有功能。额外还提供了4种方法,我们来看看其作用。
/**
* 带延迟时间的调度,只执行一次
* 调度之后可通过Future.get()阻塞直至任务执行完毕
*/
1. public ScheduledFuture> schedule(Runnable command,
long delay, TimeUnit unit);
/**
* 带延迟时间的调度,只执行一次
* 调度之后可通过Future.get()阻塞直至任务执行完毕,并且可以获取执行结果
*/
2. public ScheduledFuture schedule(Callable callable,
long delay, TimeUnit unit);
/**
* 带延迟时间的调度,循环执行,固定频率
*/
3. public ScheduledFuture> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
/**
* 带延迟时间的调度,循环执行,固定延迟
*/
4. public ScheduledFuture> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
具体不多做分析了,可以理解为Time的多线程版。
Spring Task、是Spring3.0内置的定时任务框架,支持Cron表达式来指定定时任务执行时间。
下面介绍在SpringBoot中使用Spring Task。
使用SpringBoot创建定时任务非常简单,目前主要有以下三种创建方式:
@Scheduled注解和@EnableScheduling注解的使用
基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。
@SpringBootApplication
@EnableScheduling //开启定时任务
public class ScheduledDemoApplication
{
public static void main(String[] args)
{
SpringApplication.run(ScheduledDemoApplication.class, args);
}
}
/**
* 创建定时任务,并使用 @Scheduled 注解。
* @author pan_junbiao
**/
@Component
public class Task
{
@Scheduled(cron="0/5 * * * * ? ") //每5秒执行一次
public void execute(){
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //设置日期格式
System.out.println("欢迎访问 pan_junbiao的博客 " + df.format(new Date()));
}
}
@Scheduled注解各参数讲解
源码如下:
package org.springframework.scheduling.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
String CRON_DISABLED = "-";
String cron() default "";
String zone() default "";
long fixedDelay() default -1L;
String fixedDelayString() default "";
long fixedRate() default -1L;
String fixedRateString() default "";
long initialDelay() default -1L;
String initialDelayString() default "";
}
(1)cron
该参数接收一个cron表达式,cron表达式是一个字符串,字符串以5或6个空格隔开,分开共6或7个域,每一个域代表一个含义。
cron 表达式语法:
格式:[秒] [分] [小时] [日] [月] [周] [年]
* 表示所有值. 例如:在分的字段上设置 "*",表示每一分钟都会触发。
? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10 * ?
- 表示区间。例如 在小时上设置 "10-12",表示 10,11,12点都会触发。
, 表示指定多个值,例如在周字段上设置 "MON,WED,FRI" 表示周一,周三和周五触发
/ 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)。在月字段上设置'1/3'所示每月1号开始,每隔三天触发一次。
L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五"
W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 "1W",它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,"W"前只能设置具体的数字,不允许区间"-").
# 序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六.注意如果指定"#5",正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了)
可通过在线生成Cron表达式的工具:在线Cron表达式生成器 来生成自己想要的表达式。
cron表达式使用占位符
另外,cron属性接收的cron表达式支持占位符。eg:
配置文件:
time:
cron: */5 * * * * *
interval: 5
每5秒执行一次:
@Scheduled(cron="${time.cron}")
void testPlaceholder1() {
System.out.println("Execute at " + System.currentTimeMillis());
}
@Scheduled(cron="*/${time.interval} * * * * *")
void testPlaceholder2() {
System.out.println("Execute at " + System.currentTimeMillis());
}
(2)zone
时区,接收一个 java.util.TimeZone#ID。cron表达式会基于该时区解析。默认是一个空字符串,即取服务器所在地的时区。比如我们一般使用的时区Asia/Shanghai。该字段我们一般留空。
(3)fixedDelay
上一次执行完毕时间点之后多长时间再执行。如:
@Scheduled(fixedDelay = 5000) //上一次执行完毕时间点之后5秒再执行
(4)fixedDelayString
与 fixedDelay 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。如:
@Scheduled(fixedDelayString = "5000") //上一次执行完毕时间点之后5秒再执行
占位符的使用:
在 application.yml 配置文件中添加如下配置:
time:
fixedDelay: 5000
/**
* 定时任务的使用
* @author pan_junbiao
**/
@Component
public class Task
{
@Scheduled(fixedDelayString = "${time.fixedDelay}")
void testFixedDelayString()
{
System.out.println("欢迎访问 pan_junbiao的博客 " + System.currentTimeMillis());
}
}
(5)fixedRate
上一次开始执行时间点之后多长时间再执行。如:
@Scheduled(fixedRate = 5000) //上一次开始执行时间点之后5秒再执行
(6)fixedRateString
与 fixedRate 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。
(7) initialDelay
第一次延迟多长时间后再执行。如:
@Scheduled(initialDelay=1000, fixedRate=5000) //第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次
(8) initialDelayString
与 initialDelay 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。
(1)在MySQL数据库中创建 cron 表,并添加数据。
DROP TABLE IF EXISTS cron;
CREATE TABLE cron (
cron_id VARCHAR(30) NOT NULL PRIMARY KEY,
cron VARCHAR(30) NOT NULL
);
INSERT INTO cron VALUES ('1', '0/5 * * * * ?');
(2)添加pom.xml配置信息
在pom.xml配置文件中添加MyBatis、 MySQL的JDBC数据库驱动依赖。
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.3
mysql
mysql-connector-java
8.0.20
(3)配置相关信息
将项目默认的application.properties文件的后缀修改为“.yml”,即配置文件名称为:application.yml,并配置以下信息:
spring:
#DataSource数据源
datasource:
url: jdbc:mysql://localhost:3306/db_admin?useSSL=false&
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
#MyBatis配置
mybatis:
type-aliases-package: com.pjb.entity #别名定义
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #指定 MyBatis 所用日志的具体实现,未指定时将自动查找
map-underscore-to-camel-case: true #开启自动驼峰命名规则(camel case)映射
lazy-loading-enabled: true #开启延时加载开关
aggressive-lazy-loading: false #将积极加载改为消极加载(即按需加载),默认值就是false
lazy-load-trigger-methods: "" #阻挡不相干的操作触发,实现懒加载
cache-enabled: true #打开全局缓存开关(二级环境),默认值就是true
4)创建定时器
数据库准备好数据之后,我们编写定时任务,注意这里添加的是TriggerTask,目的是循环读取我们在数据库设置好的执行周期,以及执行相关定时任务的内容。具体代码如下:
/**
* 动态定时任务配置类
* @author pan_junbiao
**/
@Configuration //1.主要用于标记配置类,兼备Component的效果
@EnableScheduling //2.开启定时任务
public class DynamicScheduleConfigurer implements SchedulingConfigurer
{
@Mapper
public interface CronMapper {
@Select("select cron from cron limit 1")
public String getCron();
}
//注入mapper
@Autowired
@SuppressWarnings("all")
CronMapper cronMapper;
/**
* 执行定时任务.
*/
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar)
{
taskRegistrar.addTriggerTask(
//1.添加任务内容(Runnable)
() -> System.out.println("欢迎访问 pan_junbiao的博客: " + LocalDateTime.now().toLocalTime()),
//2.设置执行周期(Trigger)
triggerContext -> {
//2.1 从数据库获取执行周期
String cron = cronMapper.getCron();
//2.2 合法性校验.
if (StringUtils.isEmpty(cron)) {
// Omitted Code ..
}
//2.3 返回执行周期(Date)
return new CronTrigger(cron).nextExecutionTime(triggerContext);
}
);
}
}
/**
* 基于注解设定多线程定时任务
* @author pan_junbiao
*/
@Component
@EnableScheduling // 1.开启定时任务
@EnableAsync // 2.开启多线程
public class MultithreadScheduleTask
{
@Async
@Scheduled(fixedDelay = 1000) //间隔1秒
public void first() throws InterruptedException {
System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
System.out.println();
Thread.sleep(1000 * 10);
}
@Async
@Scheduled(fixedDelay = 2000)
public void second() {
System.out.println("第二个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
System.out.println();
}
}
注意:由于基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。所以这里使用 @Async 注解很关键。
从控制台可以看出,第一个定时任务和第二个定时任务互不影响;
并且,由于开启了多线程,第一个任务的执行时间也不受其本身执行时间的限制,所以需要注意可能会出现重复操作导致数据异常。
上面提到了一些定时任务的解决方案都是在单机下执行的,当遇到一些复杂场景例如分布式下的分片和高可用的话,就需要用到分布式定时框架。
通常情况下,一个定时任务要涉及到以下三个角色
介绍一下常见的一些分布式定时任务框架
QuartZ | xxl-job | SchedulerX 2.0 | PowerJob | |
推荐度 | 1 | 4 | 2 | 3 |
是否有前端页面 | N | Y | Y | Y |
定时类型 | CRON | CRON | CRON、固定频率、固定延迟、OpenAPI | CRON、固定频率、固定延迟、OpenAPI |
支持数据库 | 关系型数据库 (MySQL、Oracle...) |
MySQL | 人民币(不开源) | 任意 Spring Data Jpa支持的关系型数据库(MySQL、Oracle...) |
报警监控 | 无 | 邮件 | 短信 | 邮件,提供接口允许开发者扩展 |
指定调度类型 | 不确定 | 支持 | 不确定 | 不支持 |
开发方式 | Bean里的方法上加注解 | 略复杂:单个Bean实现PowerJob的指定接口。 | ||
任务类型 | 内置Java | 内置Java、GLUE Java、Shell、Python等脚本 | 内置Java、外置Java(FatJar)、Shell、Python等脚本 | 内置Java、外置Java(容器)、Shell、Python等脚本 |
分布式任务 | 无 | 静态分片 | MapReduce 动态分片 | MapReduce 动态分片 |
在线任务治理 | 不支持 | 支持 | 支持 | 支持 |
日志白屏化 | 不支持 | 支持 | 不支持 | 支持 |
调度方式和性能 | 基于数据库锁 有性能瓶颈 |
基于数据库锁 有性能瓶颈 |
不详 | 无锁化设计,性能强劲无上限 |
DGA工作流 | 不支持 | 不支持 | 不支持 | 不支持 |
Job:一个函数式接口,其中的execute方法就是我们需要具体实现的业务任务逻辑。
JobDetail:用于绑定Job,并对Job进行描述,其中提供了很多描述性属性如:name 任务名称、group 任务组、description 任务描述、jobClass 任务类、jobDataMap 任务自定义参数等。
Tigger:触发器,用于定义Job的执行时间、执行间隔、执行频率等。在Quartz中主要有四种类型的Trigger:SimpleTrigger、CronTrigger、DataIntervalTrigger和NthIncludedTrigger。
Scheduler:调度器,用于实际协调和组织JobDetail与Trigger。Quartz提供了DirectSchedulerFactory和StdSchedulerFactory等工厂类,用于支持Scheduler相关对象的产生。
Scheduler可看成是一个定时任务调度容器,里面可注入多组任务(JobDetail与Trigger),而每个JobDetail又绑定了一个Job实例。一个JobDetail可对应多个Trigger,一个Trigger只能对应一个JobDetail。
Quartz中主要存在两类线程:即执行线程和调度线程。
执行线程通常由一个线程池维护,主要作用是执行Trigger中即将开始的任务。
调度线程又分为Regular Scheduler Thread(执行常规调度)和Misfire Scheduler Thread(执行错失的任务)。
其中Regular Thread 轮询Trigger,如果有将要触发的Trigger,则从执行任务线程池中获取一个空闲线程,然后执行与该Trigger关联的job;
Misfire Thraed则是扫描所有的trigger,查看是否有错失的,如果有的话,根据一定的策略进行处理。
ClusterManager线程:Quartz集群部署时,则还存在集群线程(ClusterManager线程),主要作用是定时检测集群中各节点健康状态。若发现宕机节点,则将其任务交由其他健康节点继续执行。
Quartz默认加载工程目录下的quartz.properties,如果工程目录下没有,就会去加载quartz.jar包下面的quartz.properties文件,也可自定义配置位置。
配置属性大体可分为:
以整合springboot为例,贴出核心配置:
# 定时任务的表前缀
org.quartz.jobStore.tablePrefix=qrtz_
# 是否是集群的任务
org.quartz.jobStore.isClustered=true
# 检查集群的状态间隔
org.quartz.jobStore.clusterCheckinInterval=5000
# 如果当前的执行周期被错过 任务持有的时长超过此时长则认为任务过期,单位ms
org.quartz.jobStore.misfireThreshold=6000
# 事务的隔离级别 推荐使用默认级别 设置为true容易造成死锁和不可重复读的一些事务问题
org.quartz.jobStore.txIsolationLevelSerializable=false
# 任务存储方式它应当是org.quartz.spi.JobStore的子类
org.quartz.jobStore.class=org.springframework.scheduling.quartz.LocalDataSourceJobStore
# 保证待执行的任务是锁定的 避免集群任务被其他现场抢断
org.quartz.jobStore.acquireTriggersWithinLock=true
# 数据库系统的方言StdJDBCDelegate标准JDBC方言
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# 定时任务实例的id 默认自动
org.quartz.scheduler.instanceId=AUTO
# 定时任务的线程名,相同集群实例名称必须相同
org.quartz.scheduler.instanceName=ClusterJMVCScheduler
# 定时任务线程池
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
# 线程池的线程总数 默认10
org.quartz.threadPool.threadCount=10
# 线程池的优先级
org.quartz.threadPool.threadPriority=5
# 自创建父线程
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true
针对CronTrigger和SimpleTrigger过失策略分别如下:
(1)CronTrigger
(2)SimpleTrigger
(3)核心策略枚举说明
(4)默认策略
CronTrigger和SimpleTrigger默认采用MISFIRE_INSTRUCTION_SMART_POLICY大致意思是“把处理逻辑交给聪明的Quartz去决定”。基本策略是
以Quartz和spring整合为例,当spring容器启动时,就会装载相关的bean。SchedulerFactoryBean实现了InitializingBean接口,因此在初始化bean的时候,会执行afterPropertiesSet方法,该方法将会调用SchedulerFactory(DirectSchedulerFactory 或者 StdSchedulerFactory,通常用StdSchedulerFactory)创建Scheduler。
SchedulerFactory在创建quartzScheduler的过程中,将会读取配置参数,初始化各个组件,关键组件如下:
- org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
- org.quartz.threadPool.threadCount=3
- org.quartz.threadPool.threadPriority=5
另外,SchedulerFactoryBean还实现了SmartLifeCycle接口,因此初始化完成后,会执行start()方法,该方法将主要会执行以下的几个动作:
Quartz持久化即将trigger和job基于jdbc存入数据库。Quartz中有两种存储方式:RAMJobStore,JobStoreSupport,其中RAMJobStore是将trigger和job存储在内存中,而JobStoreSupport是基于jdbc将trigger和job存储到数据库中。RAMJobStore的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在集群应用中,必须使用JobStoreSupport。
集成时,执行去Quartz官网下载对应数据库sql文件导入并开启Quartz持久化配置即可:
Quartz集群是基于数据库实现,主要利用了数据库的悲观锁机制。一个Quartz集群中的每个节点是一个独立的Quartz应用,它又管理着其他的节点。这就意味着你必须对每个节点分别启动或停止。Quartz集群中,独立的Quartz节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一Quartz应用的。
在大型分布式系统中,为了避免Quartz集群表和业务表之间互相影响,导致数据库性能和Quartz集群、业务系统稳定性,建议是Quartz独立出数据库或独立出定时任务系统。
(1)Job无法注入spring容器其他bean
Quartz中每次执行任务时,会由JobFactory重新创建一个新Job实例,此实例默认采用反射newInstance创建且并未交给spring管理,所以在实例化时也无法注入其他spring bean。
可通过自定JobFactory方式解决,当然在与springboot整合时,QuartzAutoConfiguration自动配置类已经帮我们处理了。
(2)集群环境下时间同步问题
Quartz实际并不关心你是在相同还是不同的机器上运行节点。当集群放置在不同的机器上时,称之为水平集群。节点跑在同一台机器上时,称之为垂直集群。对于垂直集群,存在着单点故障的问题。这对高可用性的应用来说是无法接受的,因为一旦机器崩溃了,所有的节点也就被终止了。对于水平集群,存在着时间同步问题。
节点用时间戳来通知其他实例它自己的最后检入时间。假如节点的时钟被设置为将来的时间,那么运行中的Scheduler将再也意识不到那个结点已经宕掉了。另一方面,如果某个节点的时钟被设置为过去的时间,也许另一节点就会认定那个节点已宕掉并试图接过它的Job重运行。最简单的同步计算机时钟的方式是使用某一个Internet时间服务器(Internet Time Server ITS)。
(3)节点争抢Job问题
因为Quartz使用了一个随机的负载均衡算法,Job以随机的方式由不同的实例执行。Quartz官网上提到当前,还不存在一个方法来指派(钉住) 一个 Job 到集群中特定的节点。
(4)从集群获取Job列表问题
当前,如果不直接进到数据库查询的话,还没有一个简单的方式来得到集群中所有正在执行的Job列表。请求一个Scheduler实例,将只能得到在那个实例上正运行Job的列表。Quartz官网建议可以通过写一些访问数据库JDBC代码来从相应的表中获取全部的Job信息。
基于mysql数据库搭建Quartz集群,并整合springboot、mybatisplus实现一个轻量企业级定时任务框架。
(1)自定义JOB注解,并设置启动装载所有job
package com.zkc.quartzdemo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author [email protected]
* @version 1.0.0
* @since 2021-04-15
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ScheduleAnn {
/**
* 定时任务的名称
*/
String name() default "";
/**
* 定时任务的定时表达式
*/
String cronExpression() default "";
/**
* 定时任务所属群组
*/
String group() default "";
/**
* 当前定时任务的描述
*/
String description() default "";
}
package com.zkc.quartzdemo.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author [email protected]
* @version 1.0.0
* @since 2021-04-15
*/
@Configuration
public class QuartzConfig {
@Bean(initMethod = "init")
@ConditionalOnMissingBean
public QuartzInit bootStarter() {
return new QuartzInit();
}
}
package com.zkc.quartzdemo.config;
import com.zkc.quartzdemo.annotation.ScheduleAnn;
import com.zkc.quartzdemo.dto.ScheduleJobVO;
import com.zkc.quartzdemo.service.QuartzService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.reflections.Reflections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import java.util.List;
import java.util.regex.Pattern;
/**
* @author [email protected]
* @version 1.0.0
* @since 2021-04-15
*/
@Slf4j
public class QuartzInit {
@Value("${spring.quartz.package-scan:com.zkc}")
private String packageScan;
private final static Pattern expressionPattern = Pattern.compile("\\$\\{(.*)}");
@Autowired
private QuartzService quartzService;
public void init() {
List allJobClassNames = quartzService.getAllScheduleJobClassNames();
new Reflections(packageScan).getTypesAnnotatedWith(ScheduleAnn.class).forEach(jobClass -> {
String jobClassName = jobClass.getName();
if (allJobClassNames.contains(jobClassName)) return;
// 如果当前jobClass不是org.quartz.Job的子类则不做插入任务操作
if (!Job.class.isAssignableFrom(jobClass)) {
log.error("类[{}]未继承[org.quartz.Job]无法初始化为定时任务。", jobClassName);
return;
}
ScheduleAnn annotate = jobClass.getAnnotation(ScheduleAnn.class);
String name = annotate.name();
String cronExpression = annotate.cronExpression();
String group = annotate.group();
String description = annotate.description();
ScheduleJobVO scheduleJobVO = new ScheduleJobVO()
.setClassName(jobClassName)
.setName(name)
.setCron(cronExpression)
.setGroup(group)
.setDescription(description);
// 只创建数据库中不存在的定时任务 存在的则不做创建更新
boolean existsFlag = quartzService.scheduleExists(name, group);
if (!existsFlag) {
quartzService.addJob(scheduleJobVO);
quartzService.addInitJob(jobClassName);
}
});
}
}
(2)定义调度servcie 与 controller
package com.zkc.quartzdemo.service;
import com.zkc.quartzdemo.dto.*;
import java.util.List;
/**
* @author [email protected]
* @version 1.0.0
* @since 2021-04-15
*/
public interface QuartzService {
List getAllScheduleJobClassNames();
boolean scheduleExists(String name, String group);
void addInitJob(String jobClassName);
boolean addJob(ScheduleJobVO scheduleJobVO);
boolean addDynamicScheduleJob(ScheduleAddCO scheduleAddCO);
List getAllJobGroups();
List getJobDetails();
boolean updateScheduleJob(ScheduleUpdateCO scheduleUpdateCO);
boolean deleteDynamicScheduleById(String id);
List getScheduleJobs(ScheduleQryCO scheduleQryCo);
boolean changeScheduleJobStatus(ScheduleStatusCO statusCO);
List getSystemAllJobs();
}
package com.zkc.quartzdemo.controller;
import com.zkc.quartzdemo.common.MultiResponse;
import com.zkc.quartzdemo.common.SingleResponse;
import com.zkc.quartzdemo.dto.*;
import com.zkc.quartzdemo.service.QuartzService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
/**
* @author [email protected]
* @version 1.0.0
* @since 2021-04-15
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/admin/api/schedule_job")
@Validated
public class ScheduleController {
private final QuartzService quartzService;
/**
* 添加一个新的定时任务
*
* @param scheduleAddCO 任务详情
* @return 任务添加成功与否
*/
@PostMapping(value = "/create")
public SingleResponse addNewSchedule(@Valid @RequestBody ScheduleAddCO scheduleAddCO) {
return SingleResponse.of(quartzService.addDynamicScheduleJob(scheduleAddCO));
}
/**
* 获取任务的组集合
*
* @return 组s
*/
@GetMapping(value = "/groups")
public MultiResponse findAllScheduleJobGroups() {
return MultiResponse.ofWithoutTotal(quartzService.getAllJobGroups());
}
/**
* 更新定时任务
*
* @param scheduleUpdateC0 任务详情
* @return 任务添加成功与否
*/
@PostMapping(value = "/update")
public SingleResponse updateScheduleJob(@Valid @RequestBody ScheduleUpdateCO scheduleUpdateC0) {
return SingleResponse.of(quartzService.updateScheduleJob(scheduleUpdateC0));
}
/**
* 删除定时任务
*
* @param id 任务的id
*/
@GetMapping(value = "/remove")
public SingleResponse deleteScheduleJob(@NotEmpty String id) {
return SingleResponse.of(quartzService.deleteDynamicScheduleById(id));
}
/**
* 获取定时任务列表
*
* @param scheduleQryCO 搜索条件
* @return 定时任务列表
*/
@GetMapping(value = "/list")
public MultiResponse getScheduleJobs(ScheduleQryCO scheduleQryCO) {
return MultiResponse.ofWithoutTotal(quartzService.getScheduleJobs(scheduleQryCO));
}
/**
* 更新定时任务状态
*
* @param statusCO 任务状态
* @return 定时任务状态更新成功与否
*/
@PostMapping(value = "/updateScheduleJobStatus")
public SingleResponse changeScheduleJobStatus(@Valid @RequestBody ScheduleStatusCO statusCO) {
return SingleResponse.of(quartzService.changeScheduleJobStatus(statusCO));
}
/**
* 任务集合
*
* @return 继承Job的SpringBean集合信息
*/
@GetMapping(value = "/jobs")
public MultiResponse getSystemAllJobs() {
return MultiResponse.ofWithoutTotal(quartzService.getSystemAllJobs());
}
}
(3)除了quartz官方持久化表,新增一个job初始化表
DROP TABLE IF EXISTS `qrtz_auto_initialized`;
CREATE TABLE `qrtz_auto_initialized` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_class_name` varchar(500) DEFAULT NULL,
`init_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
完整代码已分享到码云
https://gitee.com/zhang_kaicheng/quartz-demo
Quartz从入门到精通(最详细基础-进阶-实战)_quartz 学习-CSDN博客