本来计划做一次应用的部署升级,由单机模式,改为集群模式。但是在考虑方案时,除了遇到的SpringBoot优雅退出问题,还有一个需要考虑的问题,就是Quartz定时任务的处理。
单机模式下,quartz定时任务很简单,按照文档使用即可,使用RAM模式保存数据,不需要数据库保存数据,也不涉及任务调度加锁问题。
但是在集群模式下,quartz就需要考虑开启cluster模式了,从而避免同一个任务,被多个服务器进行触发。
quartz开启cluster模式只需要进行简单的配置即可,但是必须启用JDBC持久化存储(也可以使用Terracotta,但是太小众)。
对于quartz的cluster如何配置,网上很多,不再赘述。本文主要说明一下遇到的几个问题。
1、如何使用springboot与quartz集成
在非springboot下,我们需要自行创建SchedulerFactoryBean,并且指定jobFactory等一些必要的配置。
但是在SpringBoot下,我们可以借助autoconfiguration,自动完成这些bean的创建,只需要在yml里面进行简单配置即可。
首先,引入依赖包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
通过QuartzAutoConfiguration类,我们可以看到schedulerFactoryBean对象的初始化过程。其中所支持的一些配置方式,都在QuartzProperties中有描述。
然后在yml(或者properties)里对quartz进行配置:
spring:
# --------Quartz相关配置
quartz:
scheduler-name: XXXQuartzScheduler
wait-for-jobs-to-complete-on-shutdown: true
job-store-type: jdbc
jdbc:
initialize-schema: never
properties:
org:
quartz:
scheduler:
instanceId: AUTO
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
#dataSource: xxxxDataSource
useProperties: true
tablePrefix: T_QRTZ_
isClustered: true
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
除了spring.quartz.propertis下的配置(会原样传递至quartz,与quartz的配置是一致的),其他配置都是springboot进行包装后的quartz相关的配置,与quartz的配置其实是对应的。
具体的参数说明,网上都有介绍,不再赘述。
2、如果按照上面的配置,启动工程会报错,提示未配置DataSource。
quartz默认的是自行管理JDBC connection pool,目前支持的包括C3P0(默认)、HarikiCP,当然也可以进行自定义,只需要实现org.quartz.utils.ConnectionProvider即可。
对于我们应用中用到的Druid,目前Druid也自带了Quartz的支持,提供了DruidQuartzConnectionProvider来使用,具体可以参考:
这种方式实现后,quartz单独管理着所需的数据库连接池。
(!!!需要注意:必须指定org.quartz.dataSource,且需要配置该名称的DataSource,具体配置方式参考:)quartz/docs/configuration.adoc at main · quartz-scheduler/quartz · GitHubCode for Quartz Scheduler. Contribute to quartz-scheduler/quartz development by creating an account on GitHub.https://github.com/quartz-scheduler/quartz/blob/master/docs/configuration.adoc#quartz-created-datasources-are-defined-with-the-following-properties但是,对于我们的应用来说,quartz并没有单独部署集群,而是集成在应用内的,quartz相关的数据库表也创建在应用本身的数据库中。所以并不希望再重新创建一个独立的数据库连接池,而是能够直接复用应用本身的连接池。
quartz似乎没有提供指定连接池DataSource对象的配置参数。org.quartz.dataSource指定的是通过quartz配置的DataSource的name。
查看了SchedulerFactoryBean类的源码,其实scheduler提供了setDataSource方法,用来指定DataSource对象。但是QuartzAutoConfiguration里面并没有提供配置DataSource的方式。
那解决方案有两种:
一是不再使用AutoConfiguration生成的schedulerFactoryBean,而是自行创建。然后设置所需的DataSource。但是这种方式就没法用springboot提供的自动装配功能了。
另一种方式就是借助SchedulerFactoryBeanCustomizer,这个接口是SpringBoot quartz autoconfiguration提供给的一个定制化配置schedulerFactoryBean的口子。
通过这个方式,可以完美解决指定DataSource对象的问题。
@Component
public class QuartzSchedulerFactoryBean implements SchedulerFactoryBeanCustomizer {
@Resource
private SimpleJobListener simpleJobListener;
@Resource
private DruidDataSource xxxDataSource;
@Override
public void customize(SchedulerFactoryBean schedulerFactoryBean) {
schedulerFactoryBean.setGlobalJobListeners(simpleJobListener);
schedulerFactoryBean.setDataSource(xxxDataSource);
}
}
说明:xxxDataSource就是在其他地方定义好的DataSource,因为都被spring托管,可以直接引用。
说明2:SimpleJobListener是自定义的一个joblistener (与此问题无关),通过指定joblistener,可以实现一些自定义的任务事件处理逻辑。示例代码如下:
@Component
public class SimpleJobListener extends JobListenerSupport {
@Autowired
private SystemJobLogService systemJobLogService;
private static final Logger logger = LoggerFactory.getLogger(SimpleJobListener.class);
@Override
public void jobToBeExecuted(JobExecutionContext context) {
// 设置起始时间
context.getMergedJobDataMap().put(JobConstant.KEY_JOB_START, new Date());
}
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
Short jobResult = JobConstant.JOB_RESULT_SUCCESS;
String errorMsg = null;
if (jobException != null) {
jobResult = JobConstant.JOB_RESULT_ERROR;
errorMsg = jobException.getMessage();
}
try {
Long jobId = (Long) context.getMergedJobDataMap().get(JobConstant.KEY_JOB_ID);
Date jobStart = (Date) context.getMergedJobDataMap().get(JobConstant.KEY_JOB_START);
String userId = context.getMergedJobDataMap().getString(JobConstant.KEY_JOB_USER_ID);
userId = userId == null ? JobConstant.JOB_USER_ID_DEFAULT : userId;
SystemJobLog systemJobLog = new SystemJobLog();
systemJobLog.setJobId(jobId);
systemJobLog.setJobResult(jobResult);
systemJobLog.setErrorMsg(errorMsg);
systemJobLog.setJobStart(jobStart);
systemJobLog.setJobEnd(new Date());
systemJobLog.setUserId(userId);
systemJobLogService.insert(systemJobLog);
} catch (Exception ex) {
logger.error("记录JOB日志错误:", ex);
}
}
@Override
public String getName() {
return getClass().getSimpleName();
}
}
参考资料:
PS:Quartz启动时的日志很有误导性,看到running locally、currently in standby mode等,还以为集群模式没生效,但是其实只要最后面有 which supports persistence. and is clusetered。
一般就说明能够正常运行了,然后查询一下数据库的那几个相关表,里面有数据,基本上就没问题了。
INFO main org.quartz.core.QuartzScheduler:294 - Scheduler meta-data: Quartz Scheduler (v2.3.2) 'TaQuartzScheduler' with instanceId 'xxx-PC21647829349100'
Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
NOT STARTED.
Currently in standby mode.
Number of jobs executed: 0
Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
Using job-store 'org.springframework.scheduling.quartz.LocalDataSourceJobStore' - which supports persistence. and is clustered.