Quartz 是一个开源的作业调度框架,它完全由 Java 写成,并设计用于 J2SE 和 J2EE 应用中。它提供了巨大的灵 活性而不牺牲简单性。你能够用它来为执行一个作业而创建简单的或复杂的调度。它有很多特征,如:数据库支持,集群,插件,EJB 作业预构 建,JavaMail 及其它,支持 cron-like 表达式等等。
本文将带领大家快速上手SpringBoot中Quartz集群的搭建。
堆是一个完全二叉树,堆中节点的值总是不大于(或不小于)其父节点的值
根节点大的堆叫大顶堆,根节点小的叫小顶堆。
获取父节点方法:数组索引/2
例:值为4节点的父节点4/2 = 2;6/2 = 3
插入尾部
上浮
尾部元素放入堆顶
下沉
大家可以进入该操作模拟网站-堆操作可视化,模拟堆的数据操作查看处理流程。
可以看作是一个数组,每一个节点可以保存一个round变量值,用来保存便利次数,例如表示在13点执行,那么第一次循环中1节点round值-1,第二次循环时执行。
优点:当任务执行时间粒度小于1小时的时候,相较于全部节点都使用堆实现,同一时间段内无任务时可以sleep掉线程,减少cpu压力。
缺点:节点还是需要全部便利一遍。
将时间轮根据时间分册为:年轮、月轮、周轮、小时轮。当执行到大轮的节点,再去执行相对应的小时间轮。
优点:进一步扩大循环周期,减少循环次数以减少cpu消耗。
class MyTimerTask extends TimerTask {
private String name;
public MyTimerTask(String name) {
this.name = name;
}
@Override public void run() {
try {
System.out.println("[ name = "+name+" ,startTime="+new Date());
Thread.sleep(3000);
System.out.println(" name = "+name+" ,endTime="+new Date()+" ]");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
@Test
public void TimerTest1(){
Timer timer = new Timer();
for(int i = 0; i < 3;i++) {
MyTimerTask timerTask = new MyTimerTask("task" + i);
//将任务入queue
timer.schedule(timerTask,new Date(),4000);
}
}
/**
测试结果:
[ name = task0 ,startTime=Sun Mar 12 12:52:11 CST 2023
name = task0 ,endTime=Sun Mar 12 12:52:14 CST 2023 ]
[ name = task1 ,startTime=Sun Mar 12 12:52:14 CST 2023
name = task1 ,endTime=Sun Mar 12 12:52:17 CST 2023 ]
[ name = task2 ,startTime=Sun Mar 12 12:52:17 CST 2023
name = task2 ,endTime=Sun Mar 12 12:52:20 CST 2023 ]
[ name = task2 ,startTime=Sun Mar 12 12:52:20 CST 2023
name = task2 ,endTime=Sun Mar 12 12:52:23 CST 2023 ]
[ name = task0 ,startTime=Sun Mar 12 12:52:23 CST 2023
name = task0 ,endTime=Sun Mar 12 12:52:26 CST 2023 ]
...
**/
public class Timer {
private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);
//构造器
public Timer(String name) {
thread.setName(name);
thread.start();
}
...
}
一个小顶堆TaskQueue存放Timertask.
一个工作线程TimerThread循环执行不断检查queue里的任务并执行,new Timer()时就开始跑,如果检查queue空则先令队列wait,schedule往queue添加元素时会唤醒队列。
schedule(TimerTask task, Date firstTime, long period) 执行时间firstTime只是预设时间,具体执行时间取决于上一个任务结束时间
public void schedule(TimerTask task, Date firstTime, long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), -period);
}
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");
// Constrain value of period sufficiently to prevent numeric
// overflow while still being effectively infinitely large.
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;
//双重锁
synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
//Timer.TimerThread执行时会使用到nextExecutionTime
//!!!! 注意到此时的nextExecutionTime没有加period !!!!!!
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}
//将任务添加入queue小顶堆,并且唤醒queue
queue.add(task);
if (queue.getMin() == task)
queue.notify();
}
}
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// 等待队列非空
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die
// 队列非空,取出queue小顶堆根节点
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
//取出后根节点后删除queue中根节点
queue.removeMin();
continue; // No action required, poll queue again
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;//未加period的期望执行时间
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
//period =0 代表不周期执行,删除任务
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
//修改时间后作为!下次执行的节点!加入queue
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
//期望的执行时间 > 当前时间时 -> 当前执行线程休息一会
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
//期望的执行时间 < 当前时间时 -> 运行当前任务
//意味着 当前任务执行时间
schedule方法添加任务时必须传入固定的时间firstTime,依赖系统时间,没有相对写法。比如每周周一执行这种相对时间无法执行。
由于Timer内工作线程是单线程,所以启动时间相同的任务无法同时启动,必须等待上一个任务执行完成,所以任务不是严格按照预设的间隔时间period执行,具体执行时间取决于任务执行时长。
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
for (int i = 0; i < 3;i++) {
scheduledThreadPool.scheduleAtFixedRate(new MyTimerTask("task"+i),0,4, TimeUnit.SECONDS);
}
}
/**
测试结果
[ name = task1 ,startTime=Sun Mar 12 13:03:25 CST 2023
[ name = task2 ,startTime=Sun Mar 12 13:03:25 CST 2023
[ name = task0 ,startTime=Sun Mar 12 13:03:25 CST 2023
name = task1 ,endTime=Sun Mar 12 13:03:28 CST 2023 ]
name = task0 ,endTime=Sun Mar 12 13:03:28 CST 2023 ]
name = task2 ,endTime=Sun Mar 12 13:03:28 CST 2023 ]
[ name = task0 ,startTime=Sun Mar 12 13:03:29 CST 2023
[ name = task2 ,startTime=Sun Mar 12 13:03:29 CST 2023
[ name = task1 ,startTime=Sun Mar 12 13:03:29 CST 2023
name = task2 ,endTime=Sun Mar 12 13:03:32 CST 2023 ]
name = task1 ,endTime=Sun Mar 12 13:03:32 CST 2023 ]
name = task0 ,endTime=Sun Mar 12 13:03:32 CST 2023 ]
**/
可以看出,由于多线程的原因,相同启动时间的任务的执行不需要等待上一次任务执行完成,三个任务可以严格按照启动时间同时执行,所以当核心线程数大于任务数时,启动时间之间间隔 = 预设的period。
在Leader-follower线程模型中每个线程有三种模式,leader,follower, processing。
在Leader-follower线程模型一开始会创建一个线程池,并且会选取一个线程作为leader线程,leader线程负责监听网络请求,其它线程为follower处于waiting状态,当leader线程接受到一个请求后,会释放自己作为leader的权利,然后从follower线程中选择一个线程进行激活,然后激活的线程被选择为新的leader线程作为服务监听,然后老的leader则负责处理自己接受到的请求(现在老的leader线程状态变为了processing),处理完成后,状态从processing转换为follower
可知这种模式下接受请求和进行处理使用的是同一个线程,这避免了线程上下文切换和线程通讯数据拷贝。
优点:避免没必要的唤醒和阻塞操作,更高效和节省资源。
三个核心类
JobDetail 任务类
Trigger 触发器
Scheduler 调度器
org.springframework.boot
spring-boot-starter-parent
2.3.1.RELEASE
org.springframework.boot
spring-boot-starter-quartz
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-autoconfigure
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-data-jpa
public class MyJob implements Job {
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
try {
System.out.println("[ MyJob execute : startTime="+new Date());
Thread.sleep(3000);
System.out.println(" MyJob end : endTime="+new Date()+" ]");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public class QuartzTest {
public static void main(String[] args) throws SchedulerException {
//1. create jobs
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("job1","jobGroup1")
.build();
//2. create triggers
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger","triggerGroup")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(4).repeatForever()
)
.build();
//3. create scheduler,add Job、trigger,execute
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start();
}
}
[ MyJob execute : startTime=Sun Mar 12 17:07:43 CST 2023
MyJob end : endTime=Sun Mar 12 17:07:46 CST 2023 ]
17:07:47.341 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'jobGroup1.job1', class=cn.yihui.MyJob
17:07:47.342 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
17:07:47.342 [DefaultQuartzScheduler_Worker-2] DEBUG org.quartz.core.JobRunShell - Calling execute on job jobGroup1.job1
[ MyJob execute : startTime=Sun Mar 12 17:07:47 CST 2023
MyJob end : endTime=Sun Mar 12 17:07:50 CST 2023 ]
17:07:51.341 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'jobGroup1.job1', class=cn.yihui.MyJob
17:07:51.341 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
17:07:51.341 [DefaultQuartzScheduler_Worker-3] DEBUG org.quartz.core.JobRunShell - Calling execute on job jobGroup1.job1
[ MyJob execute : startTime=Sun Mar 12 17:07:51 CST 2023
MyJob end : endTime=Sun Mar 12 17:07:54 CST 2023 ]
17:07:55.342 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'jobGroup1.job1', class=cn.yihui.MyJob
17:07:55.342 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
17:07:55.342 [DefaultQuartzScheduler_Worker-4] DEBUG org.quartz.core.JobRunShell - Calling execute on job jobGroup1.job1
[ MyJob execute : startTime=Sun Mar 12 17:07:55 CST 2023
MyJob end : endTime=Sun Mar 12 17:07:58 CST 2023 ]
...
严格按照间隔时间执行
public class QuartzTest {
public static void main(String[] args) throws SchedulerException {
//1. create jobs
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("job1","jobGroup1")
.usingJobData("testJobKey","testJobValue")
.usingJobData("test","valueJob")
.build();
//2. create triggers
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger","triggerGroup")
.usingJobData("testTriggerKey","testTriggerValue")
.usingJobData("test","valueTrigger")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(4).repeatForever()
)
.build();
//3. create scheduler,add Job、trigger,execute
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start();
}
}
public class MyJob implements Job {
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
try {
System.out.println("[ MyJob execute : startTime="+new Date());
Thread.sleep(3000);
JobDataMap jobMap = jobExecutionContext.getJobDetail().getJobDataMap();
JobDataMap triggerMap = jobExecutionContext.getTrigger().getJobDataMap();
System.out.println(jobMap.get("testJobKey"));
System.out.println(triggerMap.get("testTriggerKey"));
//合并map后重复key时,trigger的会将job的覆盖。
System.out.println(jobExecutionContext.getMergedJobDataMap().getString("test"));
System.out.println(" MyJob end : endTime="+new Date()+" ]");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
[ MyJob execute : startTime=Sun Mar 12 17:29:33 CST 2023
testJobValue
testTriggerValue
valueTrigger
MyJob end : endTime=Sun Mar 12 17:29:36 CST 2023 ]
Quartz为了并发,每次执行的jobDetail和Job实例都是不同的实例对象,相对应的JobDataMap也是不同的实例对象。
@DisallowConcurrentExecution : 禁止并发执行
@PersistJobDataAfterExecution : 持久化JobDataMap 对TriggerDataMap无效
...未完待续
...未完待续