Quartz 是一个开源的作业调度框架,它完全由 Java 写成,并设计用于 J2SE 和 J2EE 应用中。它提供了巨大的灵 活性而不牺牲简单性。你能够用它来为执行一个作业而创建简单的或复杂的调度。它有很多特征,如:数据库支持,集群,插件,EJB 作业预构 建,JavaMail 及其它,支持 cron-like 表达式等等。
本文将带领大家快速上手SpringBoot中Quartz集群的搭建。

1. 理论基础

1.1 数据结构-堆

堆是一个完全二叉树,堆中节点的值总是不大于(或不小于)其父节点的值

根节点大的堆叫大顶堆,根节点小的叫小顶堆。

堆的数组表示方式

小顶堆

获取父节点方法:数组索引/2

例:值为4节点的父节点4/2 = 2;6/2 = 3

插入数据
  • 插入尾部

  • 上浮

删除堆顶
  • 尾部元素放入堆顶

  • 下沉

大家可以进入该操作模拟网站-堆操作可视化,模拟堆的数据操作查看处理流程。

1.2 时间轮算法(cron实现原理)

round时间轮

可以看作是一个数组,每一个节点可以保存一个round变量值,用来保存便利次数,例如表示在13点执行,那么第一次循环中1节点round值-1,第二次循环时执行。

小时时间轮

优点:当任务执行时间粒度小于1小时的时候,相较于全部节点都使用堆实现,同一时间段内无任务时可以sleep掉线程,减少cpu压力。

缺点:节点还是需要全部便利一遍。

分层时间轮

将时间轮根据时间分册为:年轮、月轮、周轮、小时轮。当执行到大轮的节点,再去执行相对应的小时间轮。

分层时间轮

优点:进一步扩大循环周期,减少循环次数以减少cpu消耗。


2. JDK-Timer介绍

2.1 测试案例

    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 ]
  ...
**/

2.2 源码分析:

Timer内两个核心属性:

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方法

schedule方法

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();
}
}

Timer.TimerThread执行任务

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);
}
                //期望的执行时间 < 当前时间时 -> 运行当前任务
                //意味着 当前任务执行时间<period时,节点运行时间会连续
                //因此任务执行的时间间隔主要由firstTime和任务运行时间决定
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}
}

Timer问题

  • schedule方法添加任务时必须传入固定的时间firstTime,依赖系统时间,没有相对写法。比如每周周一执行这种相对时间无法执行。

  • 由于Timer内工作线程是单线程,所以启动时间相同的任务无法同时启动,必须等待上一个任务执行完成,所以任务不是严格按照预设的间隔时间period执行,具体执行时间取决于任务执行时长。


3. 定时任务线程池

3.1 测试案例

    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。

3.2 Leader-Follower模式

在Leader-follower线程模型中每个线程有三种模式,leader,follower, processing。
在Leader-follower线程模型一开始会创建一个线程池,并且会选取一个线程作为leader线程,leader线程负责监听网络请求,其它线程为follower处于waiting状态,当leader线程接受到一个请求后,会释放自己作为leader的权利,然后从follower线程中选择一个线程进行激活,然后激活的线程被选择为新的leader线程作为服务监听,然后老的leader则负责处理自己接受到的请求(现在老的leader线程状态变为了processing),处理完成后,状态从processing转换为follower


可知这种模式下接受请求和进行处理使用的是同一个线程,这避免了线程上下文切换和线程通讯数据拷贝。

优点:避免没必要的唤醒和阻塞操作,更高效和节省资源。


4. Quartz使用

4.1 quick-start

三个核心类

  • JobDetail 任务类

  • Trigger 触发器

  • Scheduler 调度器

mavne坐标

 <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>

Java代码

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 ]
...

严格按照间隔时间执行

4.2 JobDataMap

Java代码

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 ]

4.3 Job并发及持久化

Quartz为了并发,每次执行的jobDetail和Job实例都是不同的实例对象,相对应的JobDataMap也是不同的实例对象。

  • @DisallowConcurrentExecution : 禁止并发执行

  • @PersistJobDataAfterExecution : 持久化JobDataMap 对TriggerDataMap无效

4.4 触发器

...未完待续

4.5 SpringBoot整合Quartz

...未完待续

[框架应用系列:Quartz快速上手] Java定时任务解决方案之Quartz集群的更多相关文章

  1. 【Kubernetes 系列四】Kubernetes 实战:管理 Hello World 集群

    目录 1. 创建集群 1.1. 安装 kubectl 1.1.1. 安装 kubectl 到 Linux 1.1.2. 安装 kubectl 到 macOS 1.1.3. 安装 kubectl 到 W ...

  2. Rancher系列文章-Rancher v2.6使用脚本实现导入集群

    概述 最近在玩 Rancher, 先从最基本的功能玩起, 目前有几个已经搭建好的 K8S 集群, 需要批量导入, 发现官网已经有批量导入的文档了. 根据 Rancher v2.6 进行验证微调后总结经 ...

  3. Quartz快速上手

    快速上手你需要干啥: 下载Quartz 安装Quartz 根据你的需要来配置Quartz 开始一个示例应用 下载和安装 The Quartz JAR Files The main Quartz lib ...

  4. 【ELK解决方案】ELK集群+RabbitMQ部署方案以及快速开发RabbitMQ生产者与消费者基础服务

    前言: 大概一年多前写过一个部署ELK系列的博客文章,前不久刚好在部署一个ELK的解决方案,我顺便就把一些基础的部分拎出来,再整合成一期文章.大概内容包括:搭建ELK集群,以及写一个简单的MQ服务. ...

  5. 基于.NetCore的Redis5.0.3(最新版)快速入门、源码解析、集群搭建与SDK使用【原创】

    1.[基础]redis能带给我们什么福利 Redis(Remote Dictionary Server)官网:https://redis.io/ Redis命令:https://redis.io/co ...

  6. 三分钟快速搭建分布式高可用的Redis集群

    这里的Redis集群指的是Redis Cluster,它是Redis在3.0版本正式推出的专用集群方案,有效地解决了Redis分布式方面的需求.当单机内存.并发.流量等遇到瓶颈的时候,可以采用这种Re ...

  7. Java+大数据开发——Hadoop集群环境搭建(二)

    1. MAPREDUCE使用 mapreduce是hadoop中的分布式运算编程框架,只要按照其编程规范,只需要编写少量的业务逻辑代码即可实现一个强大的海量数据并发处理程序 2. Demo开发--wo ...

  8. 第十节: 利用SQLServer实现Quartz的持久化和双机热备的集群模式 :

    背景: 默认情况下,Quartz.Net作业是持久化在内存中的,即 quartz.jobStore.type = "Quartz.Simpl.RAMJobStore, Quartz" ...

  9. Hadoop学习笔记1 - 使用Java API访问远程hdfs集群

    转载请标注原链接 http://www.cnblogs.com/xczyd/p/8570437.html 2018年3月从新司重新起航了.之前在某司过了的蛋疼三个月,也算给自己放了个小假了. 第一个小 ...

  10. Java+大数据开发——Hadoop集群环境搭建(一)

    1集群简介 HADOOP集群具体来说包含两个集群:HDFS集群和YARN集群,两者逻辑上分离,但物理上常在一起 HDFS集群: 负责海量数据的存储,集群中的角色主要有 NameNode / DataN ...

随机推荐

  1. (数据科学学习手札164)在vscode中调用Deepseek进行AI辅助编程

    本文示例配置文件已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 大家好我是费老师,最近国产大模型Deepse ...

  2. MYSQL查询:指定数值A表中B表没有对应数值

    MYSQL查询:指定数值A表中B表没有对应数值 在A表根据指定的arrange列的33743439, 33734907,33563462, 33563939等数值查询数据,连接B表,最后查询的结果只返 ...

  3. 日志数据采集-Flume

    1. 前言 在一个完整的离线大数据处理系统中,除了hdfs+mapreduce+hive组成分析系统的核心之外,还需要数据采集.结果数据导出.任务调度等不可或缺的辅助系统,而这些辅助工具在hadoop ...

  4. 数据结构 Trick 之:子树 k 距离内问题

    能够解决的题目类型 这个 Trick 能解决的题目形如: 给定 \(n\) 个节点的有根无边权有点权树. 有 \(m\) 个询问,每个询问形如点 \(x\) 的子树内与 \(x\) 深度差不超过 \( ...

  5. SSH 跳板机原理与配置:实现无缝跳板连接,一步直达目标主机

    前言 在日常运维或开发工作中,我们常常需要访问部署在内网的服务器.然而出于安全策略或网络拓扑的限制,内网服务器并不会直接向外部暴露端口,导致我们无法"直连"它们.此时,跳板机(Ju ...

  6. 原生input上传视拼,参数形式 file: (binary)形式的

    <input type="file" @change="demo"> if(e.target.files[0]&&e.target. ...

  7. 分布式事务之2PC两阶段提交

    1. 分布式事务概述 1.1 问题背景 在分布式系统中,业务操作可能跨越多个服务或数据库(如订单服务.库存服务.支付服务),传统单机事务(ACID)无法满足跨网络节点的数据一致性需求. 网络不可靠:服 ...

  8. DeepSeek-R1本地部署如何选择适合你的版本?看这里

    DeepSeek-R1本地部署:选择最适合你的版本,轻松搞定! 关于本地部署DeepSeek-R1前期知识 如果你正在考虑将DeepSeek-R1部署到本地服务器上,了解每种类型的硬件需求是非常重要的 ...

  9. RocketMQ实战—10.营销系统代码优化

    大纲 1.营销系统引入MQ实现异步化来进行性能优化 2.基于MQ释放优惠券提升系统扩展性 3.基于Redis实现重复促销活动去重 4.基于促销活动创建事件实现异步化 5.推送任务分片和分片消息batc ...

  10. 记录一次修复 JetBrains Rider 控制台输出乱码

    在使用 JetBrains Rider 调试程序时,控制台输出日志出现了乱码. 歪打正着结果困扰许久的问题得到了解决,于是记录下了这个小短文. 具体的修复建议如下:将终端编码设置为 GB2312 具体 ...