任务调度之Quartz(二)

Quartz集成到Spring

Spring-quartz 工程
Spring 在 spring-context-support.jar 中直接提供了对 Quartz 的支持。
在这里插入图片描述
可以在配置文件中把 JobDetail、Trigger、Scheduler 定义成 Bean。

定义Job

<bean name="myJob1" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="name" value="my_job_1"/>
<property name="group" value="my_group"/>
<property name="jobClass" value="com.gupaoedu.quartz.MyJob1"/>
<property name="durability" value="true"/>
</bean>

定义Trigger

<bean name="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<property name="name" value="my_trigger_1"/>
<property name="group" value="my_group"/>
<property name="jobDetail" ref="myJob1"/>
<property name="startDelay" value="1000"/>
<property name="repeatInterval" value="5000"/>
<property name="repeatCount" value="2"/> </bean>

定义Scheduler

<bean name="scheduler" 
class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers">
<list>
	<ref bean="simpleTrigger"/>
	<ref bean="cronTrigger"/>
</list>
</property>
</bean>

既然可以在配置文件配置,当然也可以用@Bean 注解配置。在配置类上加上
@Configuration 让 Spring 读取到。

public class QuartzConfig {
	@Beanpublic 
	JobDetail printTimeJobDetail(){ 
		return JobBuilder.newJob(MyJob1.class)
			.withIdentity("gupaoJob")
			.usingJobData("gupao", "职位更好的你")
			.storeDurably().build();
	}
@Bean
public Trigger printTimeJobTrigger() {
	CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?"); 
	return TriggerBuilder.newTrigger()
	.forJob(printTimeJobDetail())
	.withIdentity("quartzTaskService")
	.withSchedule(cronScheduleBuilder)
	.build();
	}
}

运行 spring-quartz 工程的 com.gupaoedu.quartz.QuartzTest

动态调度的实现

springboot-quartz 工程
传统的 Spring 方式集成,由于任务信息全部配置在 xml 文件中,如果需要操作任务或者修改任务运行频率,只能重新编译、打包、部署、重启,如果有紧急问题需要处理,会浪费很多的时间。
有没有可以动态调度任务的方法?比如停止一个 Job?启动一个 Job?修改 Job 的触发频率?读取配置文件、写入配置文件、重启 Scheduler 或重启应用明显是不可取的。
对于这种频繁变更并且需要实时生效的配置信息,我们可以放到哪里?
ZK、Redis、DB tables。
并且,我们可以提供一个界面,实现对数据表的轻松操作。

配置管理

这里我们用最简单的数据库的实现。
问题 1:建一张什么样的表?参考 JobDetail 的属性。

CREATE TABLE `sys_job` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`job_name` varchar(512) NOT NULL COMMENT '任务名称',
`job_group` varchar(512) NOT NULL COMMENT '任务组名',
`job_cron` varchar(512) NOT NULL COMMENT '时间表达式',
`job_class_path` varchar(1024) NOT NULL COMMENT '类路径,全类型',
`job_data_map` varchar(1024) DEFAULT NULL COMMENT '传递 map 参数',
`job_status` int(2) NOT NULL COMMENT '状态:1 启用 0 停用',
`job_describe` varchar(1024) DEFAULT NULL COMMENT '任务功能描述', 
PRIMARY KEY (`id`)) 
ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8;

数据操作与任务调度

操作数据表非常简单,SSM 增删改查。
但是在修改了表的数据之后,怎么让调度器知道呢?调度器的接口:Scheduler
在我们的需求中,我们需要做的事情:

  1. 新增一个任务
  2. 删除一个任务
  3. 启动、停止一个任务
  4. 修改任务的信息(包括调度规律)

因 此 可 以 把 相 关 的 操 作 封 装 到 一 个 工 具 类 中 。
com.gupaoedu.demo.util.SchedulerUtil

前端界面

在这里插入图片描述
接下来我们有两个问题要解决:

容器启动与Service注入

容器启动

因为任务没有定义在 ApplicationContext.xml 中,而是放到了数据库中,Spring
Boot 启动时,怎么读取任务信息?或者,怎么在 Spring 启动完成的时候做一些事情?
创建一个类,实现 CommandLineRunner 接口,实现 run 方法。
从表中查出状态是 1 的任务,然后构建。

Service类注入到Job中

Spring Bean 如何注入到实现了 Job 接口的类中?
例如在 TestTask3 中,需要注入 ISysJobService,查询数据库发送邮件。
如果没有任何配置,注入会报空指针异常。
原因:
因为定时任务 Job 对象的实例化过程是在 Quartz 中进行的,而 Service Bean 是由 Spring 容器管理的,Quartz 察觉不到 Service Bean 的存在,所以无法将 Service Bean 装配到 Job 对象中。
分析:
Quartz 集成到 Spring 中,用到 SchedulerFactoryBean,其实现了 InitializingBean 方法,在唯一的方法 afterPropertiesSet()在 Bean 的属性初始化后调用。
调度器用 AdaptableJobFactory 对 Job 对象进行实例化。所以,如果我们可以把这个 JobFactory 指定为我们自定义的工厂的话,就可以在 Job 实例化完成之后,把 Job 纳入到 Spring 容器中管理。
解决这个问题的步骤:

  1. 定义一个 AdaptableJobFactory,实现 JobFactory 接口,实现接口定义的 newJob 方法,在这里面返回 Job 实例
  2. 定义一个 MyJobFactory,继承 AdaptableJobFactory。
    使用 Spring 的 AutowireCapableBeanFactory,把 Job 实例注入到容器中。
@Componentpublic class MyJobFactory extends AdaptableJobFactory {
	@Autowiredprivate 
	AutowireCapableBeanFactory capableBeanFactory;protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
	Object jobInstance = super.createJobInstance(bundle);
	capableBeanFactory.autowireBean(jobInstance);return jobInstance;
	}
}
  1. 指定 Scheduler 的 JobFactory 为自定义的 JobFactory。 com.gupaoedu.demo.config.InitStartSchedule 中:
scheduler.setJobFactory(myJobFactory);

考虑这么一种情况:正在运行的 Quartz 节点挂了,而所有人完全不知情……

Quartz集群部署

springboot-quartz 工程

为什么需要集群?

  1. 防止单点故障,减少对业务的影响
  2. 减少节点的压力,例如在 10 点要触发 1000 个任务,如果有 10 个节点,则每个节点之需要执行 100 个任务

集群需要解决的问题?

  1. 任务重跑,因为节点部署的内容是一样的,到 10 点的时候,每个节点都会执行相同的操作,引起数据混乱。比如跑批,绝对不能执行多次。
  2. 任务漏跑,假如任务是平均分配的,本来应该在某个节点上执行的任务,因为节点故障,一直没有得到执行。
  3. 水平集群需要注意时间同步问题
  4. Quartz 使用的是随机的负载均衡算法,不能指定节点执行

所以必须要有一种共享数据或者通信的机制。在分布式系统的不同节点中,我们可以采用什么样的方式,实现数据共享?两两通信,或者基于分布式的服务,实现数据共享。
例如:ZK、Redis、DB。
在 Quartz 中,提供了一种简单的方式,基于数据库共享任务执行信息。也就是说,一个节点执行任务的时候,会操作数据库,其他的节点查询数据库,便可以感知到了。
同样的问题:建什么表?哪些字段?依旧使用系统自带的 11 张表。

集群配置与验证

quartz.properties 配置。
四个配置:集群实例 ID、集群开关、数据库持久化、数据源信息
注意先清空 quartz 所有表、改端口、两个任务频率改成一样验证 1:先后启动 2 个节点,任务是否重跑验证 2:停掉一个节点,任务是否漏跑

Quartz调度原理

问题:

  1. Job 没有继承 Thread 和实现 Runnable,是怎么被调用的?通过反射还是什么?
  2. 任务是什么时候被调度的?是谁在监视任务还是监视 Trigger?
  3. 任务是怎么被调用的?谁执行了任务?
  4. 任务本身有状态吗?还是触发器有状态?

看源码的入口

Scheduler scheduler = factory.getScheduler(); 
scheduler.scheduleJob(jobDetail, trigger); 
scheduler.start();

获取调度器实例

读取配置文件
public Scheduler getScheduler() throws SchedulerException {
	if (cfg == null) {
	// 读取 quartz.properties 配置文件 
	initialize();
}
// 这个类是一个 HashMap,用来基于调度器的名称保证调度器的唯一性
SchedulerRepository schedRep = SchedulerRepository.getInstance();​Scheduler sched = schedRep.lookup(getSchedulerName());
// 如果调度器已经存在了
if (sched != null) {
// 调度器关闭了,移除 
if (sched.isShutdown()) { 
schedRep.remove(getSchedulerName());} else {
// 返回调度器return sched;}}
// 调度器不存在,初始化
sched = instantiate();return sched;
}

instantiate()方法中做了初始化的所有工作:

// 存储任务信息的 JobStore
JobStore js = null;
// 创建线程池,默认是 SimpleThreadPool
ThreadPool tp = null;
// 创建调度器
QuartzScheduler qs = null;
// 连接数据库的连接管理器
DBConnectionManager dbMgr = null;
// 自动生成 ID
// 创建线程执行器,默认为 DefaultThreadExecutor
ThreadExecutor threadExecutor;
创建线程池(包工头)

830 行和 839 行,创建了一个线程池,默认是配置文件中指定的

SimpleThreadPool。
String tpClass = cfg.getStringProperty(PROP_THREAD_POOL_CLASS, SimpleThreadPool.class.getName()); tp = (ThreadPool) loadHelper.loadClass(tpClass).newInstance();

SimpleThreadPool 里面维护了三个 list,分别存放所有的工作线程、空闲的工作线程和忙碌的工作线程。我们可以把 SimpleThreadPool 理解为包工头。

private List<WorkerThread> workers;
private LinkedList<WorkerThread> availWorkers = new LinkedList<WorkerThread>(); 
private LinkedList<WorkerThread> busyWorkers = new LinkedList<WorkerThread>();

tp 的 runInThread()方法是线程池运行线程的接口方法。参数 Runnable 是执行的
任务内容。取出 WorkerThread 去执行参数里面的 runnable(JobRunShell)。

WorkerThread wt = (WorkerThread)availWorkers.removeFirst(); 
busyWorkers.add(wt);
wt.run(runnable);
WorkerThread(工人)

WorkerThread 是 SimpleThreadPool 的 内 部 类 , 用 来 执 行 任 务 。 我 们 把 WorkerThread理解为工人。在WorkerThread的 run 方法中,执行传入的参数runnable 任务:

runnable.run();
创建调度线程(项目经理)

1321 行,创建了调度器 QuartzScheduler:

qs = new QuartzScheduler(rsrcs, idleWaitTime, dbFailureRetry);

在 QuartzScheduler 的构造函数中,创建了 QuartzSchedulerThread,我们把它理解为项目经理,它会调用包工头的工人资源,给他们安排任务。
并 且 创 建 了 线 程 执 行 器 schedThreadExecutor , 执 行 了 这 个
QuartzSchedulerThread,也就是调用了它的 run 方法。

// 创建一个线程,resouces 里面有线程名称
this.schedThread = new QuartzSchedulerThread(this, resources);
// 线程执行器
ThreadExecutor schedThreadExecutor = resources.getThreadExecutor();
//执行这个线程,也就是调用了线程的 run 方法
schedThreadExecutor.execute(this.schedThread);

点开 QuartzSchedulerThread 类,找到 run 方法,这个是 Quartz 任务调度的核心方法:

public void run() { 
int acquiresFailed = 0;
// 检查 scheuler 是否为停止状态 
while (!halted.get()) { 
try {
// check if we're supposed to pause...
synchronized (sigLock) {
// 检查是否为暂停状态
while (paused && !halted.get()) { 
try {
// wait until togglePause(false) is called...
// 暂停的话会尝试去获得信号锁,并 wait 一会 
sigLock.wait(1000L);} 
catch (InterruptedException ignore) { }// reset failure counter when paused, so that we don't
// wait again after unpausing acquiresFailed = 0;}​
if (halted.get()) { break;}}// wait a bit, if reading from job store is consistently 
// failing (e.g. DB is down or restarting)..
// 从 JobStore 获取 Job 持续失败,sleep 一下
if (acquiresFailed > 1) { 
try { 
	long delay = computeDelayForRepeatedErrors(qsRsrcs.getJobStore(), acquiresFailed); 
	Thread.sleep(delay);
} catch (Exception ignore) {}}
// 从线程池获取可用的线程
int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads(); if(availThreadCount > 0) { 
// will always be true, due to semantics of blockForAvailableThreads...​
List<OperableTrigger> triggers;long now = System.currentTimeMillis();clearSignaledSchedulingChange(); 
try {
// 获取需要下次执行的 triggers
// idleWaitTime:默认 30s
// availThreadCount:获取可用(空闲)的工作线程数量,总会大于 1,因为该方法会一直阻塞,直到有工作线程空闲下来。
// maxBatchSize:一次拉取 trigger 的最大数量,默认是 1
// batchTimeWindow:时间窗口调节参数,默认是 0
// misfireThreshold:超过这个时间还未触发的 trigger,被认为发生了 misfire,默认 60s
// 调度线程一次会拉取 NEXT_FIRETIME 小于(now + idleWaitTime +batchTimeWindow),大于(now - misfireThreshold)的,min(availThreadCount,maxBatchSize)个 triggers,默认情况下,会拉取未来 30s、过去 60s 之间还未 fire 的 1 个 trigger
triggers = qsRsrcs.getJobStore().acquireNextTriggers( now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()),qsRsrcs.getBatchTimeWindow());// 省略…………// set triggers to 'executing'
List<TriggerFiredResult> bndles = new ArrayList<TriggerFiredResult>();boolean goAhead = true; synchronized(sigLock) { goAhead = !halted.get();} if(goAhead) { 
try {
// 触发 Trigger,把 ACQUIRED 状态改成 EXECUTING
// 如果这个 trigger 的 NEXTFIRETIME 为空,也就是未来不再触发,就将其状态改为COMPLETE
// 如果 trigger 不允许并发执行(即 Job 的实现类标注了@DisallowConcurrentExecution),则将状态变为 BLOCKED,否则就将状态改为 WAITING
List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
// 省略…………
continue;
}}
// 循环处理 
Triggerfor (int i = 0; i < bndles.size(); i++) {                                              TriggerFiredResult result =   bndles.get(i);                                              TriggerFiredBundle bndle =   result.getTriggerFiredBundle();Exception exception = result.getException();// 省略…………​
JobRunShell shell = null; 
try {
// 根据 trigger 信息实例化 JobRunShell(implements Runnable),同时依据 JOB_CLASS_NAME 实例化 Job,随后我们将 JobRunShell 实例丢入工作线。
shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle); shell.initialize(qs);
} 
catch (SchedulerException se) { qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(),CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR); 
continue;
}
// 执行 JobRunShell 的 run 方法
if (qsRsrcs.getThreadPool().runInThread(shell) == false) {// 省略…………

JobRunShell 的作用
JobRunShell instances are responsible for providing the ‘safe’ environment for Job s to run in, and for performing all of the work of executing the Job, catching ANY thrown exceptions, updating the Trigger with the Job’s completion code, etc.
A JobRunShell instance is created by a JobRunShellFactory on behalf of the QuartzSchedulerThread which then runs the shell in a thread from the configured ThreadPool when the scheduler determines that a Job has been triggered.
JobRunShell 用来为 Job 提供安全的运行环境的,执行 Job 中所有的作业,捕获运行中的异常,在任务执行完毕的时候更新 Trigger 状态,等等。
JobRunShell 实例是用 JobRunShellFactory 为 QuartzSchedulerThread 创建的,在调度器决定一个 Job 被触发的时候,它从线程池中取出一个线程来执行任务。

线程模型总结

SimpleThreadPool:包工头,管理所有 WorkerThread
WorkerThread:工人,把 Job 包装成 JobRunShell,执行
QuartSchedulerThread:项目经理,获取即将触发的 Trigger,从包工头出拿到 worker,执行 Trigger 绑定的任务

绑定JobDetail和Trigger

// 存储 JobDetail 和 Trigger 
resources.getJobStore().storeJobAndTrigger(jobDetail, trig);
// 通知相关的 Listener
notifySchedulerListenersJobAdded(jobDetail); notifySchedulerThread(trigger.getNextFireTime().getTime()); notifySchedulerListenersSchduled(trigger);

启动调度器

// 通知监听器
notifySchedulerListenersStarting();
if (initialStart == null) { 
	initialStart = new Date(); 
	this.resources.getJobStore().schedulerStarted(); 
	startPlugins();
} else { 
	resources.getJobStore().schedulerResumed(); 
}
// 通知 QuartzSchedulerThread 不再等待,开始干活
schedThread.togglePause(false);
// 通知监听器
notifySchedulerListenersStarted();

源码总结

getScheduler 方法创建线程池 ThreadPool,创建调度器 QuartzScheduler,创建
调度线程 QuartzSchedulerThread,调度线程初始处于暂停状态。
scheduleJob 将任务添加到 JobStore 中。
scheduler.start()方法激活调度器,QuartzSchedulerThread 从 timeTrriger 取出待触 发 的 任 务 , 并 包 装 成 TriggerFiredBundle , 然 后 由 JobRunShellFactory 创 建 TriggerFiredBundle 的 执 行 线 程 JobRunShell , 调 度 执 行 通 过 线 程 池
SimpleThreadPool 去执行 JobRunShell,而 JobRunShell 执行的就是任务类的 execute 方法:job.execute(JobExecutionContext context)。

集群原理基于数据库,如何实现任务的不重跑不漏跑?

问题 1:如果任务执行中的资源是“下一个即将触发的任务”,怎么基于数据库实现
这个资源的竞争?
问题 2:怎么对数据的行加锁?
在这里插入图片描述
QuartzSchedulerThread 第 287 行,获取下一个即将触发的 Trigger

triggers = qsRsrcs.getJobStore().acquireNextTriggers(

调用 JobStoreSupport 的 acquireNextTriggers()方法,2793 行调用 JobStoreSupport.executeInNonManagedTXLock()方法,3829 行:

return executeInNonManagedTXLock(lockName,

尝试获取锁,3843 行:

transOwner = getLockHandler().obtainLock(conn, lockName);

下面有回滚和释放锁的语句,即使发生异常,锁同样能释放。
调用 DBSemaphore 的 obtainLock()方法,103 行

public boolean obtainLock(Connection conn, String lockName) throws LockException { 
if (!isLockOwner(lockName)) { 
	executeSQL(conn, lockName, expandedSQL, expandedInsertSQL);

调用 StdRowLockSemaphore 的 executeSQL()方法,78 行。最终用 JDBC 执行 SQL,语句内容是 expandedSQL 和 expandedInsertSQL。

ps = conn.prepareStatement(expandedSQL);

问题:expandedSQL 和 expandedInsertSQL 是一条什么 SQL 语句?似乎我们没有赋值?
在 StdRowLockSemaphore 的构造函数中,把定义的两条 SQL 传进去:

public StdRowLockSemaphore() {
super(DEFAULT_TABLE_PREFIX, null, SELECT_FOR_LOCK, INSERT_LOCK);
}
public static final String SELECT_FOR_LOCK = "SELECT * FROM "+ TABLE_PREFIX_SUBST + TABLE_LOCKS + " WHERE " + COL_SCHEDULER_NAME + " = " +SCHED_NAME_SUBST+ " AND " + COL_LOCK_NAME + " = ? FOR UPDATE";public static final String INSERT_LOCK = "INSERT INTO "+ TABLE_PREFIX_SUBST + TABLE_LOCKS + "(" + COL_SCHEDULER_NAME + ", " +COL_LOCK_NAME + ") VALUES ("+ SCHED_NAME_SUBST + ", ?)";

它调用了父类 DBSemaphore 的构造函数:

public DBSemaphore(String tablePrefix, String schedName, String defaultSQL, String defaultInsertSQL) { 
	this.tablePrefix = tablePrefix; 
	this.schedName = schedName;
	setSQL(defaultSQL);
	setInsertSQL(defaultInsertSQL); 
}

在 setSQL()和 setInsertSQL()中为 expandedSQL 和 expandedInsertSQL 赋值。执行的 SQL 语句:

select * from QRTZ_LOCKS t where t.lock_name='TRIGGER_ACCESS' for update

在我们执行官方的建表脚本的时候,QRTZ_LOCKS 表,它会为每个调度器创建两行数据,获取 Trigger 和触发 Trigger 是两把锁:
在这里插入图片描述

任务为什么重复执行

在我们的演示过程中,有多个调度器,任务没有重复执行,也就是默认会加锁,什么情况下不会上锁呢?
JobStoreSupport 的 executeInNonManagedTXLock()方法
如果 lockName 为空,则不上锁

if (lockName != null) {
// If we aren't using db locks, then delay getting DB connection 
// until after acquiring the lock since it isn't needed.
if (getLockHandler().requiresConnection()) { 
conn = getNonManagedTXConnection();
}
transOwner = getLockHandler().obtainLock(conn, lockName);}
if (conn == null) { conn = getNonManagedTXConnection(); }

而上一步 JobStoreSupport 的 acquireNextTriggers()方法,

  1. 如 果 acquireTriggersWithinLock=true 或 者 batchTriggerAcquisitionMaxCount>1 时 , locaName 赋 值 为 LOCK_TRIGGER_ACCESS,此时获取 Trigger 会加锁。
  2. 否则,如果 isAcquireTriggersWithinLock()值是 false 并且 maxCount=1 的话, lockName 赋值为 null,这种情况获取 Trigger 下不加锁。
public List<OperableTrigger> acquireNextTriggers(final long noLaterThan, final int maxCount, final long timeWindow) throws JobPersistenceException {
String lockName;
if(isAcquireTriggersWithinLock() || maxCount > 1) { 
lockName = LOCK_TRIGGER_ACCESS;
} 
else { lockName = null;}

acquireTriggersWithinLock 变量默认是 false:

private boolean acquireTriggersWithinLock = false;

maxCount 来自 QuartzSchedulerThread:

triggers = qsRsrcs.getJobStore().acquireNextTriggers( now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()),
qsRsrcs.getBatchTimeWindow());

getMaxBatchSize()来自 QuartzSchedulerResources,代表 Scheduler 一次拉取 trigger 的最大数量,默认是 1:

private int maxBatchSize = 1;

这个值可以通过参数修改,代表允许调度程序节点一次获取(用于触发)的触发器
的最大数量,默认值是 1。

org.quartz.scheduler.batchTriggerAcquisitionMaxCount=1

根据以上两个默认值,理论上在获取 Trigger 的时候不会上锁,但是实际上为什么没有出现频繁的重复执行问题?因为每个调度器的线程持有锁的时间太短了,单机的测试无法体现,而在高并发的情况下,有可能会出现这个问题。
QuartzSchedulerThread 的 triggersFired()方法:

List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);

调用了 JobStoreSupport 的 triggersFired()方法,接着又调用了一个 triggerFired
triggerFired(Connection conn, OperableTrigger trigger)方法:
如果 Trigger 的状态不是 ACQUIRED,也就是说被其他的线程 fire 了,返回空。但是这种乐观锁的检查在高并发下难免会出现 ABA 的问题,比如线程 A 拿到的时候还是 ACQUIRED 状态,但是刚准备执行的时候已经变成了 EXECUTING 状态,这个时候就会出现重复执行的问题。

if (!state.equals(STATE_ACQUIRED)) { 
	return null;
}

总结,如果:
如果设置的数量为 1(默认值),并且使用 JDBC JobStore(RAMJobStore 不支持分 布 式 , 只 有 一 个 调 度 器 实 例 , 所 以 不 加 锁 ) , 则 属 性 org.quartz.jobStore.acquireTriggersWithinLock 应设置为 true。否则不加锁可能会导致任务重复执行。

org.quartz.scheduler.batchTriggerAcquisitionMaxCount=1 
org.quartz.jobStore.acquireTriggersWithinLock=true
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值