在说 quartz 之前,我们先回顾一下 spring 的定时任务,使用相当简单,默认集成在 spring boot 中,所以在 spring boot 项目中无需额外添加依赖,无需配置,只需要加个注解就可以了,当然也可以实现动态添加删除定时任务,详情前往上一篇博文SpringBoot-定时任务,那为什么要使用 quartz 呢,主要还是考虑分布式的应用,下面我们就来看一下 spring boot 是怎么集成 quartz 的。

介绍

简单介绍下 quartz,Quartz是OpenSymphony开源组织在任务调度领域的一个开源项目,完全基于Java实现。作为一个优秀的开源调度框架,Quartz具有以下特点:

  • (1)强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
  • (2)灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
  • (3)分布式和集群能力。

Tips
还记得 spring boot 的三板斧吗?加依赖,写配置,添注解

加依赖

引入 spring-boot-starter-quartz 的依赖

1
2
3
4
5
<!--quartz定时任务依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

写配置

无需配置

加注解

无需注解

实现

我们需要新建一个 QuartzService 的类,由于代码较长,在这里就不贴了,想看具体代码,请前往QuartzService.java。quartz 的 Job 实现方式很多,不一定要用这种方式,只是笔者认为这是一种比较简单的实现。

新加一个测试类 QuartzTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringBootTest
public class QuartzTest {
private static final Logger logger = LoggerFactory.getLogger(QuartzTest.class);
@Autowired
private QuartzService quartzService;
@Test
public void quartzTest() {
logger.info("添加定时任务");
String jobName = "test-1";
Map<String, Object> map = new HashMap<>();
map.put("test", "测试任务执行");
map.put("name", jobName);
quartzService.deleteJob(jobName, "test");
quartzService.addJob(TestQuartz.class, jobName, "test", "0 */2 * * * ?", map);
}
}

新建 Job 类,需要继承 QuartzJobBean

1
2
3
4
5
6
7
8
9
10
11
12
public class TestQuartz extends QuartzJobBean {
private static final Logger logger = LoggerFactory.getLogger(TestQuartz.class);
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
logger.info("任务开始执行:" + formatter.format(System.currentTimeMillis()));
JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
String test = jobDataMap.get("test").toString();
String jobName = jobDataMap.get("name").toString();
logger.info(test + ":" + jobName);
}
}

启动,运行 Test 类,便可添加一个任务,创建 Job 时需要的参数可以通过 JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();来获取,这样就完成了一个定时任务功能。

Quartz 集群使用

一个 Quartz 集群中的每个节点是一个独立的 Quartz 应用,它又管理着其他的节点。这就意味着你必须对每个节点分别启动或停止。Quartz 集群中,独立的 Quartz 节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一 Quartz 应用的。

因为 Quartz 集群依赖于数据库,所以必须首先创建 Quartz 数据库表,Quartz 发布包中包括了所有被支持的数据库平台的SQL脚本。这些SQL脚本存放于 <quartz_home>/docs/dbTables 目录下,总共12张表,不同版本,表个数可能不同。下面是具体表的说明:

  • qrtz_blob_triggers : 以Blob 类型存储的触发器。
  • qrtz_calendars:存放日历信息, quartz可配置一个日历来指定一个时间范围。
  • qrtz_cron_triggers:存放cron类型的触发器。
  • qrtz_fired_triggers:存放已触发的触发器。
  • qrtz_job_details:存放一个jobDetail信息。
  • qrtz_job_listeners:job监听器。
  • qrtz_locks: 存储程序的悲观锁的信息(假如使用了悲观锁)。
  • qrtz_paused_trigger_graps:存放暂停掉的触发器。
  • qrtz_scheduler_state:调度器状态。
  • qrtz_simple_triggers:简单触发器的信息。
  • qrtz_trigger_listeners:触发器监听器。
  • qrtz_triggers:触发器的基本信息。

接下来,新建 quartz.yml 的配置文件,来覆盖默认的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
org:
quartz:
jobStore:
useProperties: false
tablePrefix: qrtz_
# 开启集群模式
isClustered: true
# 集群实例检测时间间隔 ms
clusterCheckinInterval: 5000
# misfire 任务的超时阈值 ms
misfireThreshold: 60000
txIsolationLevelReadCommitted: true
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate

scheduler:
instanceId: AUTO
rmi.export: false
rmi.proxy: false
wrapJobExecutionInUserTransaction: false

# 工作线程的线程池设置
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 25
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true

这样 Quartz 集群就可以实现了。

扩展

1、Quartz 触发时间配置的三种方式

  • cron 方式:采用cronExpression表达式配置时间。
  • simple 方式:和JavaTimer差不多,可以指定一个开始时间和结束时间外加一个循环时间。
  • calendars 方式:可以和cron配合使用,用cron表达式指定一个触发时间规律,用calendar指定一个范围。

注意:cron方式需要用到的4张数据表: qrtz_triggers,qrtz_cron_triggers,qrtz_fired_triggers,qrtz_job_details

2、使用 quartz 遇到的问题

2.1、在定时任务执行中 service @Autowired 注解不进来

创建 JobFactory 的 Bean,并在 SchedulerConfig 中添加到 SchedulerFactoryBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// JobFactory
@Component
public class JobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
@Autowired
private transient AutowireCapableBeanFactory beanFactory;
@Override
public void setApplicationContext(final ApplicationContext context) {
beanFactory = context.getAutowireCapableBeanFactory();
}
@Override
protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);
return job;
}
}

SchedulerConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
public class SchedulerConfig {
@Autowired
private DataSource dataSource;
@Autowired
private JobFactory jobFactory;
@Bean
public Properties quartzProperties() throws IOException {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.yml"));
propertiesFactoryBean.afterPropertiesSet();
return propertiesFactoryBean.getObject();
}
@Bean
public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setSchedulerName("Cluster_Scheduler");
factory.setDataSource(dataSource);
factory.setApplicationContextSchedulerContextKey("applicationContext");
factory.setQuartzProperties(quartzProperties());
factory.setJobFactory(jobFactory);
return factory;
}
}

2.2、quartz 任务激活失败

在Quartz中,当一个持久化的触发器会因为:

  1. 调度器被关闭;
  2. 线程池没有可用线程;
  3. 项目重启;
  4. 任务的串行执行;

而错过激活时间,就会发生激活失败(misfire)。

可以设置 quartz中CornTrigger使用的策略

1
2
3
4
5
6
7
8
//所有的misfile任务马上执行
public static final int MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY = -1;
//在Trigger中默认选择MISFIRE_INSTRUCTION_FIRE_ONCE_NOW 策略
public static final int MISFIRE_INSTRUCTION_SMART_POLICY = 0;
// CornTrigger默认策略,合并部分misfire,正常执行下一个周期的任务。
public static final int MISFIRE_INSTRUCTION_FIRE_ONCE_NOW = 1;
//所有的misFire都不管,执行下一个周期的任务。
public static final int MISFIRE_INSTRUCTION_DO_NOTHING = 2;

1、 通过setMisfireInstruction方法设置misfire策略。

1
2
3
4
5
6
7
8
CronTriggerFactoryBean triggerFactoryBean = new CronTriggerFactoryBean();
triggerFactoryBean.setName("corn_" + clazzName);
triggerFactoryBean.setJobDetail(jobFactory.getObject());
triggerFactoryBean.setCronExpression(quartzCorn);
triggerFactoryBean.setGroup(QUARTZ_TRIGGER_GROUP);
//设置misfire策略
triggerFactoryBean.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY);
triggerFactoryBean.afterPropertiesSet();

2、 也可以通过CronScheduleBuilder设置misfire策略。

1
2
3
4
5
6
7
CronScheduleBuilder csb = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
//MISFIRE_INSTRUCTION_DO_NOTHING
csb.withMisfireHandlingInstructionDoNothing();
//MISFIRE_INSTRUCTION_FIRE_ONCE_NOW
csb.withMisfireHandlingInstructionFireAndProceed();//(默认)
//MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
csb.withMisfireHandlingInstructionIgnoreMisfires();

最后

任务调度在实际项目中会经常用到,Quartz 也是我们的不二选择,但是在高可用的系统中也存在不少问题,具体问题小伙伴们可以在留言区留言,我们一起共同探讨。

参考与相关链接

示例代码:https://github.com/dddreams/learn-spring-boot/tree/master/spring-boot-quartz

Quartz 官网:http://www.quartz-scheduler.org/

Quartz集群原理及配置应用:https://www.cnblogs.com/xiang–liu/p/10120105.html

ddAnswer

更多文章请关注微信公众号: zhiheng博客

如果文章对你有用,转发分享、点赞赞赏才是真爱 [斜眼笑]