个人专题目录
一、实现方法
spring中实现的定时任务,大致有四种方法:
- web接口,使用crontab调用
- 使用@Scheduled注解
- 使用Quartz
- java类继承TimerTask
这四类的比较如下:
方法 | 配置项 | 集群模式 | 使用场景 |
---|---|---|---|
web接口 | 较少 | 集群模式下需要申请域名,通过域名调用 | web项目 |
@Scheduled注解 | 少 | 不支持集群模式,集群模式下每个节点都会调用注解标示的任务 | 单节点项目 |
TimerTask | 少 | 不支持集群模式,集群模式下每个节点都会调用注解标示的任务 | 单节点项目 |
Quartz | 多 | 需要额外数据库来支持集群模式 | 分布式项目 |
下面详细介绍下基于注解和quartz的使用方法。
二、@Scheduled注解
2.1 依赖包
Scheduled依赖包
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
2.2 配置文件
Schedule配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:task="http://www.springframework.org/schema/task"
xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.1.xsd">
<!-- 开启定时任务 -->
<task:annotation-driven />
<!-- 配置线程池,若不配置所有的定时任务会串行执行 -->
<task:annotation-driven scheduler="myScheduler"/>
<task:scheduler id="myScheduler" pool-size="5"/>
<!-- 开启注解 -->
<context:annotation-config />
<!-- 指定相关的包路径 -->
<context:component-scan base-package="com.spring.task"/> <!-- MyTask中使用注解 -->
<!-- 当然你也可以不在java类中使用注解,此时需要如下配置 -->
<bean id="myTask" class="com.spring.task.MyTask2"></bean> <!-- MyTask2中不使用注解,在配置文件配置执行信息 -->
<task:scheduled-tasks>
<!-- 这里表示的是每隔五秒执行一次 -->
<task:scheduled ref="myTask2" method="show" cron="*/5 * * * * ?" />
<task:scheduled ref="myTask2" method="print" cron="*/10 * * * * ?"/>
</task:scheduled-tasks>
</beans>
2.3 Job类实现
Scheduled任务类
package com.spring.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 基于注解的定时器任务
*/
@Component
public class MyTask {
/**
* 定时计算。每天凌晨 01:00 执行一次
*/
@Scheduled(cron = "0 0 1 * * *")
public void show() {
System.out.println("show method 2");
}
/**
* 启动时执行一次,之后每隔2秒执行一次
*/
@Scheduled(fixedRate = 1000*2)
public void print() {
System.out.println("print method 2");
}
}
非注解任务 展开原码
package com.spring.task;
/**
* 不使用注解,自定义任务,此时就是普通的java类
*/
public class MyTask2 {
public void show() {
System.out.println("show method 1");
}
public void print() {
System.out.println("print method 1");
}
}
三、Quartz
3.1 Quartz任务调度的基本实现原理
Quartz是OpenSymphony开源组织在任务调度领域的一个开源项目,完全基于Java实现。作为一个优秀的开源调度框架,Quartz具有以下特点:
(1)强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
(2)灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
(3)分布式和集群能力,Terracotta收购后在原来功能基础上作了进一步提升。本文将对该部分相加阐述。
3.1.1 Quartz 核心元素
Quartz任务调度的核心元素为:Scheduler——任务调度器、Trigger——触发器、Job——任务。其中trigger和job是任务调度的元数据,scheduler是实际执行调度的控制器。
Trigger是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz中主要提供了四种类型的trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和NthIncludedDayTrigger。这四种trigger可以满足企业应用中的绝大部分需求。
Job用于表示被调度的任务。主要有两种类型的job:无状态的(stateless)和有状态的(stateful)。对于同一个trigger来说,有状态的job不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job主要有两种属性:requestsRecovery和durability,其中requestsRecovery表示任务是否在发生故障的时候在其他节点执行,而durability表示在没有trigger关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。一个job可以被多个trigger关联,但是一个trigger只能关联一个job。
Scheduler由scheduler工厂创建:DirectSchedulerFactory或者StdSchedulerFactory。第二种工厂StdSchedulerFactory使用较多,因为DirectSchedulerFactory使用起来不够方便,需要作许多详细的手工编码设置。Scheduler主要有三种:RemoteMBeanScheduler,RemoteScheduler和StdScheduler。
Quartz核心元素之间的关系如图所示:
核心元素关系图
3.1.2 Quartz 线程视图
在Quartz中,有两类线程,Scheduler调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。
Quartz线程视图
Scheduler调度线程主要有两个:执行常规调度的线程,和执行misfiredtrigger的线程。常规调度线程轮询存储的所有trigger,如果有需要触发的trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该trigger关联的任务。Misfire线程是扫描所有的trigger,查看是否有misfiredtrigger,如果有的话根据misfire的策略分别处理(fire now OR wait for the next fire)。
3.1.3 Quartz Job数据存储
Quartz中的trigger和job需要存储下来才能被使用。Quartz中有两种存储方式:RAMJobStore,JobStoreSupport,其中RAMJobStore是将trigger和job存储在内存中,而JobStoreSupport是基于jdbc将trigger和job存储到数据库中。RAMJobStore的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在集群应用中,必须使用JobStoreSupport。
3.2 Quartz集群原理
3.2.1 Quartz 集群架构
一个Quartz集群中的每个节点是一个独立的Quartz应用,它又管理着其他的节点。这就意味着你必须对每个节点分别启动或停止。Quartz集群中,独立的Quartz节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一Quartz应用的,如图所示。
Quartz集群架构
3.2.2 Quartz集群相关数据库表
因为Quartz集群依赖于数据库,所以必须首先创建Quartz数据库表,Quartz发布包中包括了所有被支持的数据库平台的SQL脚本。这些SQL脚本存放于<quartz_home>/docs/dbTables 目录下。这些表的简要介绍如和创建语句如下。
数据表说明
# 数据库所需表
# QRTZ_CALENDARS 以 Blob 类型存储 Quartz 的 Calendar 信息
# QRTZ_CRON_TRIGGERS 存储 Cron Trigger,包括Cron表达式和时区信息
# QRTZ_FIRED_TRIGGERS 存储与已触发的 Trigger 相关的状态信息,以及相联 Job的执行信息QRTZ_PAUSED_TRIGGER_GRPS 存储已暂停的 Trigger组的信息
# QRTZ_SCHEDULER_STATE 存储少量的有关 Scheduler 的状态信息,和别的Scheduler实例(假如是用于一个集群中)
# QRTZ_LOCKS 存储程序的悲观锁的信息(假如使用了悲观锁)
# QRTZ_JOB_DETAILS 存储每一个已配置的 Job 的详细信息
# QRTZ_JOB_LISTENERS 存储有关已配置的 JobListener的信息
# QRTZ_SIMPLE_TRIGGERS存储简单的Trigger,包括重复次数,间隔,以及已触的次数
# QRTZ_BLOG_TRIGGERS Trigger 作为 Blob 类型存储(用于 Quartz 用户用JDBC创建他们自己定制的 Trigger 类型,JobStore并不知道如何存储实例的时候)
# QRTZ_TRIGGER_LISTENERS 存储已配置的 TriggerListener的信息
# QRTZ_TRIGGERS 存储已配置的 Trigger 的信息
重要表说明:
调度器状态表(QRTZ_SCHEDULER_STATE)
说明:集群中节点实例信息,Quartz定时读取该表的信息判断集群中每个实例的当前状态。
instance_name:配置文件中org.quartz.scheduler.instanceId配置的名字,如果设置为AUTO,quartz会根据物理机名和当前时间产生一个名字。
last_checkin_time:上次检入时间
checkin_interval:检入间隔时间
触发器与任务关联表(qrtz_fired_triggers)
存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息。
触发器信息表(qrtz_triggers)
trigger_name:trigger的名字,该名字用户自己可以随意定制,无强行要求
trigger_group:trigger所属组的名字,该名字用户自己随意定制,无强行要求
job_name:qrtz_job_details表job_name的外键
job_group:qrtz_job_details表job_group的外键
trigger_state:当前trigger状态设置为ACQUIRED,如果设为WAITING,则job不会触发
trigger_cron:触发器类型,使用cron表达式
任务详细信息表(qrtz_job_details)
说明:保存job详细信息,该表需要用户根据实际情况初始化
job_name:集群中job的名字,该名字用户自己可以随意定制,无强行要求。
job_group:集群中job的所属组的名字,该名字用户自己随意定制,无强行要求。
job_class_name:集群中job实现类的完全包名,quartz就是根据这个路径到classpath找到该job类的。
is_durable:是否持久化,把该属性设置为1,quartz会把job持久化到数据库中
job_data:一个blob字段,存放持久化job对象。
权限信息表(qrtz_locks)
3.2.3 Quartz Scheduler在集群中的启动流程
Quartz Scheduler自身是察觉不到被集群的,只有配置给Scheduler的JDBC JobStore才知道。当Quartz Scheduler启动时,它调用JobStore的schedulerStarted()方法,它告诉JobStore Scheduler已经启动了。schedulerStarted() 方法是在JobStoreSupport类中实现的。JobStoreSupport类会根据quartz.properties文件中的设置来确定Scheduler实例是否参与到集群中。假如配置了集群,一个新的ClusterManager类的实例就被创建、初始化并启动。ClusterManager是在JobStoreSupport类中的一个内嵌类,继承了java.lang.Thread,它会定期运行,并对Scheduler实例执行检入的功能。Scheduler也要查看是否有任何一个别的集群节点失败了。检入操作执行周期在quartz.properties中配置。
3.2.4 侦测失败的Scheduler节点
当一个Scheduler实例执行检入时,它会查看是否有其他的Scheduler实例在到达他们所预期的时间还未检入。这是通过检查SCHEDULER_STATE表中Scheduler记录在LAST_CHEDK_TIME列的值是否早于org.quartz.jobStore.clusterCheckinInterval来确定的。如果一个或多个节点到了预定时间还没有检入,那么运行中的Scheduler就假定它(们) 失败了。
3.2.5 从故障实例中恢复Job
当一个Sheduler实例在执行某个Job时失败了,有可能由另一正常工作的Scheduler实例接过这个Job重新运行。要实现这种行为,配置给JobDetail对象的Job可恢复属性必须设置为true(job.setRequestsRecovery(true))。如果可恢复属性被设置为false(默认为false),当某个Scheduler在运行该job失败时,它将不会重新运行;而是由另一个Scheduler实例在下一次触发时间触发。Scheduler实例出现故障后多快能被侦测到取决于每个Scheduler的检入间隔(即2.3中提到的org.quartz.jobStore.clusterCheckinInterval)。
3.3 Quartz集群实例(Quartz+Spring)
3.3.1 依赖包
Quartz依赖
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
3.3.2 配置文件
有三类配置文件:数据源配置、quartz基本配置、quartz任务配置
3.3.2.1 数据源配置
1.jdbc配置
jdbc配置
quartz.url=jdbc:mysql://10.111.17.78:3306/kec_scheduler?useUnicode=true&characterEncoding=utf8
quartz.username=root
quartz.password=root
quartz.driverClassName=com.mysql.jdbc.Driver
2.数据源定义
数据源配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 属性文件读入 -->
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:database.properties</value>
</list>
</property>
</bean>
<!-- 数据源定义,使用c3p0 连接池 -->
<bean id="quartzDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<property name="driverClass" value="${quartz.driverClassName}" />
<property name="jdbcUrl" value="${quartz.url}" />
<property name="user" value="${quartz.username}" />
<property name="password" value="${quartz.password}" />
<property name="initialPoolSize" value="2" />
<property name="minPoolSize" value="10" />
<property name="maxPoolSize" value="20" />
<property name="acquireIncrement" value="2" />
<property name="maxIdleTime" value="1800" />
</bean>
<!-- 使用jdbc访问数据库 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="quartzDataSource" />
</bean>
</beans>
3.3.2.2 quartz基本配置
quartz基本配置
#==============================================================
#Configure Main Scheduler Properties
#==============================================================
org.quartz.scheduler.instanceName = DimServerQuartz # 可为任何值,用在 JDBC JobStore 中来唯一标识实例,但是所有集群节点中必须相同
org.quartz.scheduler.instanceId = AUTO # 基于主机名和时间戳来产生实例ID
#==============================================================
#Configure ThreadPool
#==============================================================
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
#==============================================================
#Configure JobStore
#==============================================================
org.quartz.jobStore.misfireThreshold = 60000
# JobStoreTX,将任务持久化到数据中。因为集群中节点依赖于数据库来传播 Scheduler 实例的状态,你只能在使用 JDBC JobStore 时应用 Quartz 集群。
# 这意味着你必须使用 JobStoreTX 或是 JobStoreCMT 作为 Job 存储;你不能在集群中使用 RAMJobStore。
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
# 根据选择的数据库类型不同而不同,我这里的是mysql,所以是org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_ # 表前缀
org.quartz.jobStore.maxMisfiresToHandleAtATime=10
org.quartz.jobStore.isClustered = true # 属性为 true,你就告诉了 Scheduler 实例要它参与到一个集群当中
org.quartz.jobStore.clusterCheckinInterval = 3600000 # 调度实例失效的检查时间间隔,检查间隔3600s
#==============================================================
#Configure DataSource 我通过配置文件引入数据源信息,如2.1中的配置
#==============================================================
# org.quartz.jobStore.dataSource = myDS # 别名
# org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
# org.quartz.dataSource.myDS.URL = jdbc:mysql://192.168.31.18:3306/test?useUnicode=true&characterEncoding=UTF-8
# org.quartz.dataSource.myDS.user = root
# org.quartz.dataSource.myDS.password = 123456
# org.quartz.dataSource.myDS.maxConnections = 30
org.quartz.scheduler.skipUpdateCheck = true # 不检查版本更新
# org.quartz.plugin.triggHistory.class = org.quartz.plugins.history.LoggingJobHistoryPlugin # 打印步骤信息,可在调试时使用
org.quartz.plugin.shutdownhook.class = org.quartz.plugins.management.ShutdownHookPlugin # 停止时的清理插件
org.quartz.plugin.shutdownhook.cleanShutdown = true
3.3.2.3 quartz任务配置
quartz任务配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 数据库配置文件 -->
<import resource="classpath:datasource.xml"></import>
<!-- 为了支持使用注解所写的类 -->
<bean id="jobFactory" lazy-init="false" autowire="no" class="com.test.scheduler.api.AutoWiredJobFactory"/>
<bean name="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="jobFactory" ref="jobFactory"/> <!-- 正常情况下QuartzJobBean是无法使用注解的,若想使用需引入配置,该类的实现下面会展示 -->
<property name="overwriteExistingJobs" value="true"/> <!-- 是否覆盖已有job,防止启动时重复执行停止时的任务 -->
<property name="dataSource"> <!-- 数据源配置 -->
<ref bean="quartzDataSource" />
</property>
<property name="applicationContextSchedulerContextKey" value="applicationContextKey" />
<property name="configLocation" value="classpath:quartz/quartz.properties" /> <!-- quartz基本配置 -->
<property name="triggers"> <!-- 在此添加自定义的trigger -->
<list>
<ref bean="SyncTrigger" />
</list>
</property>
</bean>
<!-- 自定义的trigger -->
<bean id="SyncTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="SyncJob" />
<property name="cronExpression" value="0/10 * * * * ?" /> <!-- 每隔10s执行一次 -->
</bean>
<!-- 自定义的job -->
<bean id="SyncJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="durability" value="true"></property> <!-- 是否持久化,集群模式时需为true -->
<property name="requestsRecovery" value="true"></property> <!-- 失败或者重启时是否在其他节点恢复 -->
<property name="jobClass" <!-- 继承QuartzJobBean的自定义任务实现类 -->
value="com.scheduler.api.SyncScheduler.SyncJob">
</property>
</bean>
</beans>
3.3.3 Job类实现
自定义的任务(job),需要继承QuartzJobBean并实现其中的executeInternal函数,另外为了支持使用注解需要一个额外的AutoWiredJobFactory类。
3.3.3.1 Job类
自定义Job
package com.scheduler.api.SyncScheduler;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Service;
@Service
@DisallowConcurrentExecution // 不允许并发执行
public class SyncJob extends QuartzJobBean {
@Autowired
private JdbcTemplate jdbcTemplate;
private Logger logger = LoggerFactory.getLogger(SyncJob.class);
@Override
public void executeInternal(JobExecutionContext jobexecutioncontext) throws JobExecutionException {
//这里执行定时调度业务
logger.info("testMethod.......1");
System.out.println("2--testMethod......."+System.currentTimeMillis()/1000);
}
}
3.3.3.2 AutoWiredJobFactory类
AutoWiredJobFactory类
package com.scheduler.api;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
/**
* 供 quartz scheduler 使用,使其支持注解
*/
public class AutoWiredJobFactory extends AdaptableJobFactory {
@Autowired
private AutowireCapableBeanFactory capableBeanFactory;
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
//调用父类方法
Object jobInstance = super.createJobInstance(bundle);
//进行注入
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}
3.4 注意事项
3.4.1 时间同步问题
Quartz实际并不关心你是在相同还是不同的机器上运行节点。当集群放置在不同的机器上时,称之为水平集群。节点跑在同一台机器上时,称之为垂直集群。对于垂直集群,存在着单点故障的问题。这对高可用性的应用来说是无法接受的,因为一旦机器崩溃了,所有的节点也就被终止了。对于水平集群,存在着时间同步问题。
节点用时间戳来通知其他实例它自己的最后检入时间。假如节点的时钟被设置为将来的时间,那么运行中的Scheduler将再也意识不到那个结点已经宕掉了。另一方面,如果某个节点的时钟被设置为过去的时间,也许另一节点就会认定那个节点已宕掉并试图接过它的Job重运行。最简单的同步计算机时钟的方式是使用某一个Internet时间服务器(Internet Time Server ITS)。
3.4.2 节点争抢Job问题
因为Quartz使用了一个随机的负载均衡算法, Job以随机的方式由不同的实例执行。Quartz官网上提到当前,还不存在一个方法来指派(钉住) 一个 Job 到集群中特定的节点。
3.4.3 从集群获取Job列表问题
当前,如果不直接进到数据库查询的话,还没有一个简单的方式来得到集群中所有正在执行的Job列表。请求一个Scheduler实例,将只能得到在那个实例上正运行Job的列表。Quartz官网建议可以通过写一些访问数据库JDBC代码来从相应的表中获取全部的Job信息。
3.5 参考文档
http://www.cnblogs.com/zhenyuyaodidiao/p/4755649.html (Quartz集群原理及配置应用)
http://veiking.iteye.com/blog/2372284 (Quartz在集群、分布式系统中的应用)
http://www.jianshu.com/p/14f86c6efe22 (分布式定时任务(二))
http://www.jianshu.com/p/a518dd3229de (分布式定时任务(三))