Quartz调度器简述、动态配置并发数、版本升级、内存或数据库配置
Quartz简介
Quartz是一个开源的任务调度框架。基于定时、定期的策略来执行任务是它的核心功能。Quartz有3个核心要素:调度器(Scheduler)、任务(Job)、触发器(Trigger)。Quartz完全使用Java开发,可以集成到各种规模的应用程序中。它能够承载成千上万的任务调度,并且支持集群。它支持将数据存储到数据库中以实现持久化,并支持绝大多数的数据库。它将任务与触发设计为松耦合,即一个任务可以对应多个触发器,这样能够轻松构造出极为复杂的触发策略。
运行机制
设置应用自启动接口(server-package1.xml)
<com.core.servlet.IBootObject caption="应用自启动接口">
<item id="TimerJobReg" caption="应用启动执行" implement="com.pansoft.timerScheduler.TimerJobReg"/>
</com.core.servlet.IBootObject>
TimerJobReg(初始化组件)
TimerJobReg:系统启动时异步注册定时任务(初始化组件,实现了 IBootObject 接口,异步调用IOMTimerTaskScheduler的registerJob 方法)
IBootObject:osp-base-sdk包里init()方法,在应用启动时会被容器或者框架自动调用
@Override
public void init() {
logger.info("定时任务初始化:"+new SimpleDateFormat("yyyyMMdd HH:mm:ss").format(new Date()));
JParamObject PO = JParamObject.Create();
CompletableFuture.runAsync(() -> {
try {
EAI.DAL.IOM("IOMTimerTaskScheduler", "registerJob", PO, null);
} catch (Exception e) {
e.printStackTrace();
}
});
}
IOMTimerTaskScheduler(门面模式)
public JResponseObject registerJob(Object Param, Object Data, Object CustomObject,
Object AdditiveObject) throws Exception {
JParamObject PO = (JParamObject) Param;
JResponseObject RO = new JResponseObject();
try {
String where = PO.GetValueByParamName("where","");
IOMTimerTaskSchedulerSvr.orgnzJobs(PO,where);
} catch (Exception e) {
e.printStackTrace();
RO.setErrorCode(-1);
RO.setErrorString(e.getMessage());
}
return RO;
}
IOMTimerTaskScheduler:继承JActiveObject,门面模式,为对外暴露的服务接口,将请求转发给内部服务类IOMTimerTaskSchedulerSvr
registerJob —启动时初始化所有定时任务
用途:系统启动时,从数据库加载所有启用的定时任务,并注册到 Quartz 调度器中
三个核心方法:
注册任务(registerJob )
修改任务(modifyJob):动态修改任务(启停,改cron表达式(时间)
手动执行任务(executeJobOnce):反射创建Job实例,构造 JobExecutionContext(模拟 Quartz 上下文),直接调用 job.execute(context)
IOMTimerTaskSchedulerSvr(核心实现类)
private static void startTimerJob(EFRowSet row) {
String taskID = row.getString("TASK_BH", "");
String taskMC = row.getString("TASK_MC", "");
String jobClass = row.getString("F_TASK_CLASS", "").trim();
String cronTime = row.getString("F_CRON", "").trim();
// 读取并发数配置,默认为 1(串行)
int maxConcurrent = row.getInt("F_MAX_CONCURRENT", 1);
// 删除旧任务(同时清理信号量)
QuartzManager.removeJob(taskID);
ConcurrentControlJobWrapper.clearSemaphore(taskID); // 清理旧信号量
// 判断是否启用
if (maxConcurrent <= 0) {
// 禁用任务:记录日志,但不计算 nextFireTime
logTaskForDisabled(taskID, taskMC, "已禁用(maxConcurrent=" + maxConcurrent + ")");
return;
}
// 验证 Cron 表达式合法性
if (cronTime.isEmpty()) {
logTaskForDisabled(taskID, taskMC, "Cron 表达式为空,跳过注册");
return;
}
// 准备 JobDataMap
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("REAL_JOB_CLASS", jobClass);
jobDataMap.put("MAX_CONCURRENT", maxConcurrent);
// 注册包装器 Job
boolean result = QuartzManager.startJob(
taskID,
ConcurrentControlJobWrapper.class,
cronTime,
jobDataMap
);
if (result) {
// 启用成功:传入合法 cronTime 计算下次执行时间
logTaskForEnabled(taskID, taskMC, cronTime, maxConcurrent);
} else {
// 注册失败:也记录日志(不计算 nextFireTime)
logTaskForDisabled(taskID, taskMC, "Quartz 注册失败");
}
}
从数据库加载任务;
根据任务状态、服务器 IP 等条件决定是否启动;
使用 Quartz 调度器注册/移除任务;
支持手动立即执行任务;
记录任务日志。
|
模块 |
作用 |
|
orgnzJobs |
初始化所有(或符合条件的)定时任务,用于系统启动或动态刷新 |
|
executeJob |
手动触发一次任务执行(不走调度器,直接调用 job.execute()) |
|
getTimerTask |
从表 SYS_TIMER_TASK_NEW 查询任务元数据 |
|
startTimerJob |
单个任务的启动逻辑:校验状态、IP、类加载、注册到 Quartz |
|
QuartzManager |
封装了 Quartz 的 Scheduler 操作(add/remove/start) |
|
Log + Jloger |
自定义日志记录,用于审计任务启停 |
QuartzManager(调度操作)
public static boolean startJob(String jobName, Class<? extends Job> jobClass,
String time, JobDataMap jobDataMap) {
boolean result = false;
try {
Scheduler sched = getScheduler();
JobDetail jobDetail = new JobDetail();
jobDetail.setName(jobName);
jobDetail.setGroup(Utils.JOB_GROUP_NAME);
jobDetail.setJobClass(jobClass);
if (jobDataMap != null) {
jobDetail.setJobDataMap(jobDataMap); // 传递配置参数
}
CronTrigger trigger = new CronTrigger(jobName, Utils.TRIGGER_GROUP_NAME);
trigger.setCronExpression(time);
sched.scheduleJob(jobDetail, trigger);
if (!sched.isShutdown()) {
sched.start();
}
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
startJob(jobName, job, time):注册一个 Job + CronTrigger 到调度器
modifyJobTime(scheduler, jobDetail, time):修改已有任务的 Cron 表达式(先删后加)
removeJob(jobName):完全移除任务(暂停触发器 → 取消调度 → 删除 Job)

StatefulJob 的作用(仅限 Quartz 1.x):
禁止同一个 JobDetail 的并发执行;
即:如果上一次 execute() 还没结束,下一次触发会被阻塞/丢弃(取决于 misfire 策略);
在 Quartz 2.x+ 中已被废弃,改用注解 @DisallowConcurrentExecution
quartz.properties在quratz-all.1.6.6内,调度信息存储在内存,重启清零
信号量是进程内共享的:适用于单机部署;若集群部署,需改用 Redis 分布式锁;
// 真正的分布式并发控制(以Redis为例)
public class DistributedConcurrentJobWrapper implements Job {
private static final String LOCK_PREFIX = "job:concurrent:";
public void execute(JobExecutionContext context) {
String jobKey = context.getJobDetail().getKey().toString();
String lockKey = LOCK_PREFIX + jobKey;
int maxConcurrent = getMaxConcurrent(context);
// 使用Redis计数(原子操作)
Long current = redisTemplate.opsForValue().increment(lockKey, 1);
if (current > maxConcurrent) {
redisTemplate.opsForValue().decrement(lockKey, 1);
return; // 超过并发,丢弃
}
try {
// 执行真正的任务
} finally {
redisTemplate.opsForValue().decrement(lockKey, 1);
}
}
}
quartz配置(quartz.properties):
Quartz 的默认加载顺序:
1.首先查找系统属性 org.quartz.config 指定的文件
2.然后在 classpath 中查找 quartz.properties 文件
3.如果都找不到,使用内置默认配置
# Default Properties file for use by StdSchedulerFactory
# to create a Quartz Scheduler Instance, if a different
# properties file is not explicitly specified.
#
1.调度器(Scheduler)基本配置
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
作用:为当前调度器实例命名。
说明:
在单机环境中可任意命名;
在集群环境中,所有节点必须使用相同的 instanceName(配合 instanceId 区分实例);
默认值就是 DefaultQuartzScheduler。
org.quartz.scheduler.rmi.export: false
作用:是否将调度器通过 RMI(远程方法调用)暴露出去。
说明:设为 true 可让其他 JVM 远程控制此调度器(极少使用),一般保持 false。
org.quartz.scheduler.rmi.proxy: false
作用:是否创建一个 RMI 代理来连接远程调度器。
说明:若你的应用要连接别人暴露的 RMI 调度器才设为 true,否则 false。
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
作用:是否将 Job 的执行包裹在 JTA 用户事务中。
说明:仅在使用 Java EE 容器 + JTA 事务时才需开启,普通 Spring Boot/Java SE 应用保持 false。
2. 线程池(ThreadPool)配置
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
作用:指定线程池实现类。
说明:
SimpleThreadPool 是 Quartz 内置的简单线程池;
也可替换为自定义线程池(如集成 Spring 的 ThreadPoolTaskExecutor,但需额外适配)。
org.quartz.threadPool.threadCount: 10
作用:线程池中最大工作线程数。
说明:
表示最多可同时执行 10 个 Job;
若有 15 个任务同时触发,5 个会排队等待;
根据业务并发量调整(一般 5~50 足够)。
org.quartz.threadPool.threadPriority: 5
作用:线程优先级(1~10,5 为普通优先级)。
说明:通常保持默认 5,除非有特殊调度需求。
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
作用:工作线程是否继承初始化线程的上下文类加载器(ContextClassLoader)。
说明:
设为 true 可避免在 Web 容器(如 Tomcat)中因类加载器隔离导致的 ClassNotFoundException;
建议保持 true,尤其在 Web 应用中。
3. 容错与补偿(Misfire)配置
org.quartz.jobStore.misfireThreshold: 60000
作用:判定任务“错过触发(misfire)”的时间阈值(单位:毫秒)。
说明:
默认 60,000 毫秒 = 60 秒;
如果一个任务本应在 10:00:00 触发,但调度器在 10:00:59 才处理它 → 未 misfire;
如果到 10:01:01 才处理 → 视为 misfire,将按 Trigger 的 misfire 策略处理(如立即执行、忽略等);
可根据任务敏感度调整(如关键任务可设为 5000 毫秒)。
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
作用:指定任务数据的存储方式。
说明:
RAMJobStore:所有 Job、Trigger、Calendar 等信息仅保存在内存(JVM Heap)中;
优点:速度快、零依赖;
缺点:应用重启后所有调度信息丢失;
若需持久化,应改为 org.quartz.impl.jdbcjobstore.JobStoreTX 并配置数据库。
内存模式(RAMJobStore) → 重启清零
- 所有调度信息(Job、Trigger、Calendar 等)仅保存在 JVM 内存中
- 优点:速度快、零依赖
- 缺点:应用重启后所有任务丢失
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
# 或 JobStoreCMT(用于 JTA 事务)
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
org.quartz.jobStore.dataSource = myDS
org.quartz.dataSource.myDS.URL = jdbc:oracle:...
org.quartz.dataSource.myDS.user = ...
org.quartz.dataSource.myDS.password = ...
数据库模式(JDBCJobStore) → 持久化,重启不丢
- 调度信息存储在 12 张 Quartz 表 中(如
QRTZ_JOB_DETAILS,QRTZ_TRIGGERS等) - 优点:持久化、支持集群
- 缺点:需建表、有数据库依赖、性能略低
只要 jobStore.class 是 RAMJobStore,就是内存模式;只要是 JDBCJobStore 相关类,就是数据库模式。
内存模式只适合:本地开发、临时脚本、非关键任务
| 维度 | 内存模式 (RAMJobStore) |
数据库模式 (JDBCJobStore) |
|---|---|---|
| 存储位置 | JVM 堆内存 | 数据库表(QRTZ_*) |
| 持久化 | ❌ 应用重启后全部丢失 | ✅ 重启后自动恢复 |
| 集群支持 | ❌ 不支持(多实例会重复执行) | ✅ 支持(通过数据库锁协调) |
| 任务动态更新 | 需手动重建(如你的 modifyJob) |
可通过 API 实时更新 |
| 依赖 | 无外部依赖 | 需要数据库 + 建表 |
| 性能 | ⚡ 极快(微秒级调度) | 🐢 略慢(毫秒级,有 DB IO) |
| 可靠性 | 低(进程崩溃即丢任务) | 高(事务保障、可审计) |
如何改成数据库存储?(持久化)
1.在数据库中创建 Quartz 表
- 脚本位置:
quartz-all-1.6.6.jar中的org/quartz/impl/jdbcjobstore/tables_*.sql - Oracle 用
tables_oracle.sql
2.提供 quartz.properties 文件(放在 classpath 根目录)
org.quartz.scheduler.instanceName = MyScheduler
org.quartz.threadPool.threadCount = 20
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
org.quartz.jobStore.useProperties = false
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.dataSource.myDS.driver = oracle.jdbc.OracleDriver
org.quartz.dataSource.myDS.URL = jdbc:oracle:thin:@localhost:1521:orcl
org.quartz.dataSource.myDS.user = your_user
org.quartz.dataSource.myDS.password = your_pass
org.quartz.dataSource.myDS.maxConnections = 20
3.确保应用启动时加载该配置
在 Quartz 2.2.x 中,可以只写 driver/url/user/password,它会用 DriverManagerConnectionProvider。
但在 2.3+ 中:
DriverManagerConnectionProvider被移除- 如果 classpath 没有 HikariCP 也没有 C3P0,Quartz 会尝试加载 C3P0
- 所以 Quartz 2.3+ 必须显式提供一个连接池实现!
解决方法:添加c3p0依赖或HikariCP依赖(高性能、轻量)
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.5</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.4.5</version> <!-- 兼容 JDK8 -->
</dependency>
两者差异比较(C3P0于2015年不再维护):
| 特性 | C3P0 | HikariCP | Druid |
|---|---|---|---|
| 性能 | 慢 | ⚡ 极快(业界最快) | 快 |
| 代码量 | >200 类 | ~130 类(精简) | 较大(功能多) |
| 并发模型 | 基于锁 | 无锁(ConcurrentBag + ThreadLocal) | 优化锁 |
| 连接获取延迟 | 高 | 极低 | 低 |
| 监控能力 | 弱 | JMX + Micrometer | ✅ 强大 Web 控制台 |
| SQL 防火墙/审计 | 无 | 无 | ✅ 支持 |
| 活跃维护 | ❌ 停滞 | ✅ 持续更新 | ✅ 阿里维护 |
| 默认安全性 | 低 | 高 | 高 |
界面配置
任务调度:SYS_TIMER_TASK_NEW
TASK_BH:任务编号
TASK_MC:任务名称
F_TASK_CLASS:类全名(含路径)
TASK_TYPE:日期条件(W:无,Y:年度,M:月度,D:日期)
H_STATUS:运行状态(RUN:启动,STOP:停止)
F_CRON:时间表达式
F_NOTE:备注
F_SERVER:服务器IP
目前只支持禁止并发(通过StatefulJob)及允许无限并发(通过普通Job)
如果需要设置并发数,需要在SYS_TIMER_TASK_NEW中添加字段设置最大并发数(如F_MAX_CONCURRENT)
- 数据库加字段(如 F_MAX_CONCURRENT);
- 引入包装器(Wrapper) + 信号量(Semaphore);
- 修改 startJob 逻辑。
- 信号量是进程内共享的:适用于单机部署;若集群部署,需改用 Redis 分布式锁;5
- 丢弃策略:当前超过并发上限时直接丢弃,如需重试可记录到延迟队列;
Corn(时间表达式)
最常见的 标准 Cron 表达式有 6 位或 7 位
Linux crontab 通常是 5 位(没有“秒”和“年”):分 时 日 月 周
Quartz(Java)默认是 6 位(含“秒”,不含“年”)或 7 位(含“年”)
Spring @Scheduled 使用的是 6 位(秒 分 时 日 月 周)
Cron 表达式是一种用于配置定时任务执行时间的字符串格式,广泛应用于 Linux 系统(如 crontab)、Java(如 Spring @Scheduled)、Quartz 调度框架等。
|
字段位置 |
含义 |
允许值 |
允许特殊字符 |
|
1 |
秒(可选) |
0–59 |
, - * / |
|
2 |
分钟 |
0–59 |
, - * / |
|
3 |
小时 |
0–23 |
, - * / |
|
4 |
日(月份中的) |
1–31 |
, - * ? / L W C |
|
5 |
月 |
1–12 或 JAN–DEC |
, - * / |
|
6 |
星期 |
0–7(0 和 7 都是周日)或 SUN–SAT |
, - * ? / L # C |
|
7(可选) |
年 |
1970–2099 |
, - * / |
特殊字符说明
|
字符 |
含义 |
|
* |
任意值(如 * 在分钟位表示每分钟) |
|
? |
不指定值,用于“日”和“星期”互斥(只能一个指定具体值) |
|
- |
范围(如 10-12 表示 10,11,12) |
|
, |
枚举(如 MON,WED,FRI) |
|
/ |
步长(如 0/5 表示从 0 开始每 5 个单位一次) |
|
L |
Last,最后(如 L 在日字段表示该月最后一天) |
|
W |
最近的工作日(周一到周五) |
|
# |
第几个星期几(如 6#3 表示每月第 3 个周五) |
常见Cron表达示例:
每天凌晨2点执行:0 0 2 * * ?
每小时整点执行:0 0 * * * ?
每5分钟执行一次:0 */5 * * * ?
每周一上午9:30执行:0 30 9 ? * MON
每月1号凌晨1点执行:0 0 1 1 * ?
工作日(周一到周五)每天8:00执行:0 0 8 ? * MON-FRI
每月最后一个工作日17:00执行:0 0 17 LW * ?
不同系统对星期的起始定义不同:
Quartz/Spring:1 = 周日,2 = 周一,…,7 = 周六
Linux crontab:0 或 7 = 周日,1 = 周一,…,6 = 周六
在线 Cron 表达式生成器
https://crontab.guru/(适用于 Linux 5 位)
https://www.freeformatter.com/cron-expression-generator-quartz.html(适用于 Quartz 6/7 位)
信号量(Semaphore)—— 许可证发放机
信号量(Semaphore)是 Java 并发包(java.util.concurrent)中一种用于控制并发访问数量的同步工具
想象一个停车场:
- 总共 3 个车位(对应 maxConcurrent = 3);
- 入口有一个发牌机(Semaphore);
- 每辆车(Job 实例)进来前必须领一张牌;
- 如果没牌了(3 个车位已满),新车只能掉头离开(丢弃)或排队等(我们选择丢弃);
- 车离开时归还牌照,下一辆才能进。
Semaphore semaphore = new Semaphore(3); // 最多 3 个许可
// 尝试获取许可
if (semaphore.tryAcquire(1, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑(占用车位)
} finally {
semaphore.release(); // 归还许可(释放车位)
}
} else {
// 没拿到许可 → 超过并发上限,丢弃本次执行
}
Semaphore 保证:同一任务最多 N 个实例同时运行
包装器(Wrapper)—— “代理执行人”
你是一个老板(调度器),要安排员工干活:
- ·员工 A、B、C 各自负责不同任务(业务 Job);
- ·但你不直接派活给他们,而是通过一个管家(Wrapper);
- ·管家手里拿着“并发许可证”(Semaphore);
- ·每次派活前,管家先检查:这个任务还能不能加人?
- · 能 → 叫对应员工干活;
- · 不能 → 直接回绝:“人手满了,这次不干”。
// 业务 Job(员工)
public class DataSyncJob implements Job { ... }
// 包装器(管家)
public class ConcurrentControlJobWrapper implements Job {
public void execute(JobExecutionContext context) {
// 1. 检查并发许可(Semaphore)
// 2. 如果允许,反射调用 DataSyncJob.execute()
}
}
ConcurrentControlJobWrapper(动态并发数实现类)
package com.pansoft.timerScheduler;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* 支持最大并发数控制的 Job 包装器(兼容 Quartz 1.x)
* 包装器(管家)
* 主要功能:
* 1.检查并发许可(Semaphore信号量)
* 2.如果允许,发射调用DataSyncJob.execute()
*/
public class ConcurrentControlJobWrapper implements Job {
// 每个 taskID 对应一个信号量(全局共享)
private static final Map<String, Semaphore> SEMAPHORE_MAP = new ConcurrentHashMap<>();
private static final Logger logger = LoggerFactory.getLogger(ConcurrentControlJobWrapper.class);
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
String taskID = context.getJobDetail().getKey().getName();
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
// 从配置中获取最大并发数
Integer maxConcurrent = (Integer) dataMap.get("MAX_CONCURRENT");
if (maxConcurrent == null || maxConcurrent <= 0) {
logger.info("[Quartz] 任务 [" + taskID + "] 被禁用(maxConcurrent=" + maxConcurrent + ")");
return;
}
/* 获取或创建该任务的信号量
公平模式(true):先请求的线程优先获得许可证(FIFO);
非公平模式(false,默认):可能“插队”,吞吐量更高但可能饿死。
在定时任务场景,公平模式更合理——避免某些任务永远抢不到资源
*/
Semaphore semaphore = SEMAPHORE_MAP.computeIfAbsent(taskID, k -> new Semaphore(maxConcurrent, true));//第二个参数 = true → 公平模式
boolean acquired = false;
try {
/*
尝试获取许可(最多等待 1 秒,避免永久阻塞调度线程)
Semaphore 类的一个带超时的尝试获取许可的方法
参数1:超时时间数值
参数2:unit(时间单位)类型:TimeUnit(枚举)
acquired=true 信号量内部的许可计数器(permits)就会立即减 1
*/
acquired = semaphore.tryAcquire(1, TimeUnit.SECONDS);
/*
* 排队等待 可能造成调度线程阻塞
* 并发数已满,新任务会一直排队
* acquired = acquire();//计数器 -1
*/
if (!acquired) {
logger.info("[Quartz] 任务 [" + taskID + "] 超过最大并发数 " + maxConcurrent + ",本次触发被丢弃");
return;
}
// 反射执行真实 Job
String realJobClass = (String) dataMap.get("REAL_JOB_CLASS");
Class<?> clazz = Class.forName(realJobClass);//通过类名加载Class对象
Job realJob = (Job) clazz.newInstance();//创建Job实例
realJob.execute(context);
} catch (Exception e) {
throw new JobExecutionException("执行任务 [" + taskID + "] 失败", e);
} finally {
if (acquired) {
semaphore.release();//计数器 +1
}
}
}
/**
* 清理指定任务的信号量(用于任务删除/更新时)
*/
public static void clearSemaphore(String taskID) {
SEMAPHORE_MAP.remove(taskID);
}
}
为什么采用丢弃策略(tryAcquire(long,Timunit))
acquire():排队等待
缺点:
-
当前 Quartz 调度线程会被挂起;
-
如果并发数已满,新任务会一直排队;
-
可能导致调度器线程池耗尽,其他任务无法触发!
tryAcquire(1, TimeUnit.SECONDS):丢弃策略
参数1:超时时间数值(long);参数2:时间单位(TimeUnit)
优点:
- 超过设置的时间就记录日志退出;
- 调度线程快速释放,不影响其他任务调度;
- 符合“丢弃超额任务,保证系统稳定” 的设计目标。
并发测试结果
并发=1

并发=3

并发=0

StatefulJob和Job的区别
StatefulJob 和普通 Job 是 Quartz 调度框架中两种不同类型的作业(Job),它们的核心区别在于 是否允许多个实例并发执行。
普通 Job(无状态作业)
允许多个实例并发执行。
如果一个任务每 3 秒执行一次,而某次执行耗时 10 秒,那么在第 6 秒、第 9 秒……会继续触发新的执行实例,与正在运行的实例并行运行。
适用于:无共享状态、线程安全、可并行处理的任务(如发送通知、日志清理等)。
StatefulJob(有状态作业,2.0已废弃)
- 禁止并发执行:同一 JobDetail 的多个触发不会重叠。
- 如果上一次执行还没完成,下一次触发会被阻塞等待,直到前一次结束。
- 相当于给 Job 加了一个隐式锁(基于 JobDetail 的 key)。
- Quartz1.x写法
// 已废弃!不要使用
public class MyStatefulJob implements StatefulJob {
public void execute(JobExecutionContext context) {
// 同一时间只会有一个实例运行
}
}
- Quartz 2.0+:使用 @DisallowConcurrentExecution
Quartz 2.0 之后,不再通过接口区分,而是通过 注解 控制并发行为:
禁止并发(相当于旧的 StatefulJob):
@DisallowConcurrentExecution // ← 关键注解!
public class MySerialJob implements Job {
public void execute(JobExecutionContext context) {
// 同一 JobDetail 不会并发执行
}
}
允许并发(默认行为,普通 Job):
// 无注解 = 允许并发
public class MyParallelJob implements Job {
public void execute(JobExecutionContext context) {
// 可能多个实例同时运行
}
}
|
特性 |
普通 Job |
旧 StatefulJob |
新方式(Quartz 2+) |
|
并发执行 |
允许 |
禁止 |
用 @DisallowConcurrentExecution 禁止 |
|
状态共享 |
无(每次新建实例) |
无(但串行) |
无(但可通过注解控制串行) |
|
是否推荐 |
推荐 |
已废弃 |
推荐使用注解 |
|
实现方式 |
实现 Job |
实现 StatefulJob |
实现 Job + 注解 |
注:文中使用版本为1.6.6
文中代码 - Quartz 1.x 写法(已废弃14年!)
JobDetail jobDetail = new JobDetail(); // Quartz 1.x
CronTrigger trigger = new CronTrigger(); // Quartz 1.x
StatefulJob接口 // Quartz 1.x,2009年后就废弃了
1.X-2.X升级
将quartz-all-1.6.6.jar替换为quartz-2.3.2.jar
Job类替换
移除移除StatefulJob,改为Job,并添加注解控制并发(@DisallowConcurrentExecution )
@DisallowConcurrentExecution
public class TJob_deleteLog implements Job {
@Override
public void execute(JobExecutionContext arg0) throws JobExecutionException {
}
}
JobDetail接口相关替换
1.X:
JobDetail job = new JobDetail("job1", MyJob.class);
CronTrigger trigger = new CronTrigger("trigger1", "group1", "0/5 * * * * ?");
2.X:
import static org.quartz.JobBuilder.*;
import static org.quartz.TriggerBuilder.*;
import static org.quartz.CronScheduleBuilder.*;
JobDetail job = newJob(MyJob.class)
.withIdentity("job1", "group1")
.build();
CronTrigger trigger = newTrigger()
.withIdentity("trigger1", "group1")
.withSchedule(cronSchedule("0/5 * * * * ?"))
.build();
注:2.X所有构建需通过Builder模式,不允许直接new
2.X中JobDetail接口不再有getName方法
1.X:String taskID = context.getJobDetail().getName();
2.X:String taskID = context.getJobDetail().getKey().getName();
QuartzManager修改
public static boolean startJob(String jobName, Class<? extends Job> jobClass, String cronExpr, JobDataMap jobDataMap) {
if (jobName == null || jobName.trim().isEmpty()) {
throw new IllegalArgumentException("jobName cannot be null or empty");
}
if (jobClass == null) {
throw new IllegalArgumentException("jobClass cannot be null");
}
if (cronExpr == null || cronExpr.trim().isEmpty()) {
throw new IllegalArgumentException("cronExpr cannot be null or empty");
}
try {
// 1. 构建 JobDetail 定义任务 要执行什么/带哪些参数/叫什么名字/属于哪个组
JobDetail job = newJob(jobClass)//job类
.withIdentity(jobName) // group 默认为 DEFAULT name+gtoup
.setJobData(jobDataMap != null ? jobDataMap : new JobDataMap())//参数 (job实现类、最大并发数)
.build();
// 2. 构建 CronTrigger 定义触发器 基于cron表达式触发任务Trigger(触发器)的实现类
CronTrigger trigger = newTrigger()
.withIdentity(jobName + "_trigger") // trigger 名称与 job 关联
.withSchedule(
cronSchedule(cronExpr.trim())
.withMisfireHandlingInstructionDoNothing() // 失效策略:跳过
)
.build();
// 3. 调度任务 注册到调度器
scheduler.scheduleJob(job, trigger);
return true;
} catch (Exception e) {
System.err.println("Failed to schedule job: " + jobName + ", cron: " + cronExpr);
e.printStackTrace();
return false;
}
}
JobDetail 是 Quartz 调度框架中的核心概念之一,它代表一个 “任务的定义” —— 即:要执行什么?带哪些参数?叫什么名字?属于哪个组?
CronTrigger 是 Quartz 中用来“按 cron 表达式定时触发任务”的调度指令,它和 JobDetail(任务定义)配对使用,共同完成“在什么时间执行什么任务”的调度需求。
JobDetail 和其他组件的关系

- 一个
JobDetail可以被多个Trigger触发(比如每天 9 点和每周一 10 点都跑同一个报表)。 - 一个
Trigger只能绑定一个JobDetail。
MockJobExecutionContext(手动执行Job的模拟上下文)
package com.pansoft.timerScheduler;
import org.quartz.*;
import java.util.Date;
/**
* 用于手动执行 Job 的模拟上下文(兼容 Quartz 2.3.2)
*/
public class MockJobExecutionContext implements JobExecutionContext {
private final JobDetail jobDetail;
private final long fireTimeMillis;
public MockJobExecutionContext(JobDetail jobDetail) {
this.jobDetail = jobDetail;
this.fireTimeMillis = System.currentTimeMillis();
}
@Override
public Scheduler getScheduler() {
return null; // 手动执行不需要
}
@Override
public Trigger getTrigger() {
return null;
}
@Override
public Calendar getCalendar() {
return null;
}
@Override
public boolean isRecovering() {
return false;
}
@Override
public TriggerKey getRecoveringTriggerKey() throws IllegalStateException {
return null;
}
@Override
public int getRefireCount() {
return 0;
}
@Override
public JobDataMap getMergedJobDataMap() {
return jobDetail.getJobDataMap(); // 关键!
}
@Override
public JobDetail getJobDetail() {
return jobDetail;
}
@Override
public Job getJobInstance() {
return null;
}
@Override
public Date getFireTime() {
return new Date(fireTimeMillis);
}
@Override
public Date getScheduledFireTime() {
return getFireTime();
}
@Override
public Date getPreviousFireTime() {
return null;
}
@Override
public Date getNextFireTime() {
return null;
}
@Override
public String getFireInstanceId() {
return "MANUAL_" + System.currentTimeMillis();
}
@Override
public void setResult(Object result) {}
@Override
public long getJobRunTime() {
return 0;
}
@Override
public void put(Object o, Object o1) {
}
@Override
public Object get(Object o) {
return null;
}
@Override
public Object getResult() {
return null;
}
// 其他方法可按需实现,多数 Job 不会用到
}
为什么升级2.X要新增类MockJobExecutionContext?
Quartz 1.x 中的 JobExecutionContext 是一个可直接实例化的 concrete class;
而 Quartz 2.x 中它变成了一个 interface(接口),且其实现类是 package-private(包私有)的,无法在外部直接创建。
优点:
完全兼容 job.execute(context) 调用;
不依赖 Quartz 内部实现;
只需实现 Job 真正用到的方法(如 getJobDetail()、getMergedJobDataMap())
其他方案
在Job实现类中新增方法供手动调用(繁琐,每个Job都需添加)
public class MyJob implements Job {
public void execute(JobExecutionContext ctx) {
doWork(ctx.getJobDetail().getJobDataMap());
}
// 新增方法供手动调用
public void executeManually(Map<String, Object> params) {
doWork(new JobDataMap(params));
}
}
IOMTimerTaskSchedulerSvr(启动及手动执行修改)
启动:
private static void startTimerJob(EFRowSet row) {
String taskID = row.getString("TASK_BH", "");
String taskMC = row.getString("TASK_MC", "");
String jobClass = row.getString("F_TASK_CLASS", "").trim();
String cronTime = row.getString("F_CRON", "").trim();
// 读取并发数配置,默认为 1(串行)
int maxConcurrent = row.getInt("F_MAX_CONCURRENT", 1);
// 删除旧任务(同时清理信号量)
QuartzManager.removeJob(taskID);
ConcurrentControlJobWrapper.clearSemaphore(taskID); // 清理旧信号量
// 判断是否启用
if (maxConcurrent <= 0) {
// 禁用任务:记录日志,但不计算 nextFireTime
logTaskForDisabled(taskID, taskMC, "已禁用(maxConcurrent=" + maxConcurrent + ")");
return;
}
// 验证 Cron 表达式合法性
if (cronTime.isEmpty()) {
logTaskForDisabled(taskID, taskMC, "Cron 表达式为空,跳过注册");
return;
}
// 准备 JobDataMap
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("REAL_JOB_CLASS", jobClass);
jobDataMap.put("MAX_CONCURRENT", maxConcurrent);
// 注册包装器 Job
boolean result = QuartzManager.startJob(
taskID,
ConcurrentControlJobWrapper.class,
cronTime,
jobDataMap
);
if (result) {
// 启用成功:传入合法 cronTime 计算下次执行时间
logTaskForEnabled(taskID, taskMC, cronTime, maxConcurrent);
} else {
// 注册失败:也记录日志(不计算 nextFireTime)
logTaskForDisabled(taskID, taskMC, "Quartz 注册失败");
}
}
/**
* 记录已启用的定时任务日志(会计算下次执行时间)
*/
private static void logTaskForEnabled(String taskID, String taskMC, String cronTime, int maxConcurrent) {
try {
Log log = new Log();
log.setTaskId(taskID);
log.setAction("TIMER_TASK");
log.setAction_des(taskMC);
log.setNextFireTime(CronUtil.getNextFireTime(cronTime)); // 安全:cronTime 来自 F_CRON 字段
log.setStatus("true");
log.setExp("已启用 [并发=" + maxConcurrent + "]");
Jloger.add(log);
} catch (Exception e) {
logger.error("记录启用任务日志失败: " + taskID);
e.printStackTrace();
}
}
/**
* 记录禁用或失败的定时任务日志(不计算下次执行时间)
*/
private static void logTaskForDisabled(String taskID, String taskMC, String reason) {
try {
Log log = new Log();
log.setTaskId(taskID);
log.setAction("TIMER_TASK");
log.setAction_des(taskMC);
log.setNextFireTime(""); // 或 null,表示无下次执行时间
log.setStatus("false");
log.setExp(reason); // F_EXP 存放状态说明
Jloger.add(log);
} catch (Exception e) {
logger.error("记录禁用任务日志失败: " + taskID);
e.printStackTrace();
}
}
手动执行:
protected static boolean executeJob(JParamObject PO, String where) throws Exception {
List<String> errList = new ArrayList<>();
boolean result = true;
try {
EFDataSet ds = getTimerTask(where);
for (int i = 0; i < ds.getRowCount(); i++) {
EFRowSet row = ds.getRowSet(i);
String taskID = row.getString("TASK_BH", "");
String taskMC = row.getString("TASK_MC", "");
String jobClass = row.getString("F_TASK_CLASS", "");
if (StringUtils.isBlank(jobClass)) continue;
try {
// 1. 创建 Job 实例
Class<?> loadedClass = Class.forName(jobClass);
// 验证是否为合法 Job
if (!Job.class.isAssignableFrom(loadedClass)) {
errList.add("任务 [" + taskID + "] 类 " + jobClass + " 未实现 Job 接口");
continue;
}
@SuppressWarnings("unchecked")
Class<? extends Job> jobClazz = (Class<? extends Job>) loadedClass;
// 创建实例(可选,用于提前验证)
Job jobInstance = jobClazz.newInstance();
// 2. 构建 JobDetail(使用 Builder)
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("NOTE", "手动执行");
jobDataMap.put("TASK_MC", taskMC);
jobDataMap.put("PO", PO);
JobDetail jobDetail = JobBuilder.newJob(jobClazz)
.withIdentity(taskID, Utils.JOB_GROUP_NAME)
.setJobData(jobDataMap)
.build();
// 3. 创建 Mock 上下文
MockJobExecutionContext mockContext = new MockJobExecutionContext(jobDetail);
// 4. 执行任务
jobInstance.execute(mockContext);
} catch (Exception e) {
String msg = "手动执行任务 [" + taskID + "] 失败: " + e.toString();
errList.add(msg);
e.printStackTrace();
}
}
if (!errList.isEmpty()) {
throw new Exception(String.join("\n", errList));
}
} catch (Exception e) {
result = false;
throw e;
}
return result;
}
日志替换
Job传参方式需修改,1.0可以直接put,2.0不支持直接存取对象,需要通过全局参数JobDataMap 存取
1.x.x
arg0.put("RESULT", "true"); //必须设置
arg0.put("DESC", "定时任务"); //任务描述
arg0.put("NOTE", "同步日期"+startDate+";"+F_EXP); //备注信息
2.x.x
JobDataMap jobDataMap = arg0.getMergedJobDataMap();
jobDataMap.put("RESULT", "true");
jobDataMap.put("DESC", "定时任务");
jobDataMap.put("NOTE", "同步日期" + newtime + ";" + F_EXP);
public static void add(JobExecutionContext context) {
try (JConnection conn = Db.getConn(); Statement st = conn.createStatement()) {
// 从 JobDataMap 获取数据
JobDataMap map = context.getMergedJobDataMap();
String taskId = context.getJobDetail().getKey().getName();
String result = getString(map, "RESULT", "");
String desc = getString(map, "DESC", "");
String note = getString(map, "NOTE", "");
String exp = getString(map, "EXP", "").replaceAll("'", "");
SimpleDateFormat sf = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
String zxsjStr = formatDate(context.getFireTime(), sf);
String sczxsjStr = formatDate(context.getPreviousFireTime(), sf);
String xczxsjStr = formatDate(context.getNextFireTime(), sf);
String jhsjStr = formatDate(context.getScheduledFireTime(), sf);
Log log = new Log();
log.setTaskId(taskId);
log.setUser("Quartz");
log.setAction("TIMER_TASK");
log.setAction_des(desc);
log.setFireTime(zxsjStr);
log.setPreviousFireTime(sczxsjStr);
log.setNextFireTime(xczxsjStr);
log.setScheduledFireTime(jhsjStr);
log.setStatus(result);
log.setExp(exp);
log.setNote(note);
Jloger.add(log);
} catch (Exception e) {
LoggerFactory.getLogger(TimerJobLogger.class).error("记录定时任务日志失败", e);
}
}
// 辅助方法
private static String getString(JobDataMap map, String key, String defaultValue) {
Object val = map.get(key);
return val == null ? defaultValue : val.toString();
}
private static String formatDate(Date date, SimpleDateFormat format) {
return date == null ? "" : format.format(date);
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)