Spring整合Quartz浅析

来源:互联网 发布:java调用odata 编辑:程序博客网 时间:2024/06/06 19:09

Quartz概念基础

Quartz 是 OpenSymphony 开源组织在任务调度领域的一个开源项目,完全基于 Java 实现。

核心元素概念:

  • Job: 是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中;

  • JobDetail: Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。

  • Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等;

  • Calendar: org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合(可以简单地将org.quartz.Calendar看作java.util.Calendar的集合——java.util.Calendar代表一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。一个Trigger可以和多个Calendar关联,以便排除或包含某些时间点。假设,我们安排每周星期一早上10:00执行任务,但是如果碰到法定的节日,任务则不执行,这时就需要在Trigger触发机制的基础上使用Calendar进行定点排除。

  • Scheduler:代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。可以通过SchedulerFactory创建一个Scheduler实例。Scheduler拥有一个SchedulerContext,它类似于ServletContext,保存着Scheduler上下文信息,Job和Trigger都可以访问SchedulerContext内的信息。SchedulerContext内部通过一个Map,以键值对的方式维护这些上下文数据,SchedulerContext为保存和获取数据提供了多个put()和getXxx()的方法。可以通过Scheduler# getContext()获取对应的SchedulerContext实例;

  • ThreadPool: Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。

  • Misfire:错过的,指本来应该被执行但实际没有被执行的任务调度

为什么要选择Quartz

  • 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
  • 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
  • 支持分布式环境,不用担心每个节点都触发执行任务,而是只会负载均衡其中一个节点
  • 容错机制,job失败(如关机、程序崩溃,程序异常不算)重启恢复
  • 利用线程池,对比传统的Timer,一个任务一个线程
  • 完全由Java写成,方便集成(Spring)

Spring整合Quartz

Step1、maven依赖引入

<properties>    <springframework.version>4.2.0.RELEASE</springframework.version>    <quartz.version>2.2.1</quartz.version>    <logback.version>1.1.1</logback.version></properties><dependencies>    <!-- Spring framework -->    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-context-support</artifactId>        <version>${springframework.version}</version>    </dependency>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-tx</artifactId>        <version>${springframework.version}</version>    </dependency>    <!-- Quartz framework -->    <dependency>        <groupId>org.quartz-scheduler</groupId>        <artifactId>quartz</artifactId>        <version>${quartz.version}</version>    </dependency>        <!-- Log -->    <dependency>        <groupId>ch.qos.logback</groupId>        <artifactId>logback-classic</artifactId>        <version>${logback.version}</version>    </dependency></dependencies>

Step2、配置调度任务Job

spring提供两种方式创建Job:

方式一:MethodInvokingJobDetailFactoryBean

这种方式最简单,targetObject指定任务Bean,targetMethod指定任务执行的方法:

<bean id="simpleJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">    <property name="targetObject" ref="simpleJob" />    <property name="targetMethod" value="execute" />    <!-- 任务没处理完,下一轮又触发了,是否并发执行,默认true -->    <property name="concurrent" value="false"/></bean>

如上配置,表示任务执行simpleJob中的execute方法,simpleJob内容如下:

package example;import org.springframework.stereotype.Component;import java.util.Date;@Componentpublic class SimpleJob {    public void execute(){        System.out.println(new Date() + " : SimpleJob start...");    }}

注意: simpleJob的注入使用了自动扫描的方式,需要配置@Component注解和启用bean扫描:

<context:component-scan base-package="example"/>
方式二:JobDetailFactoryBean
<bean name="complexJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">    <property name="jobClass" value="example.ComplexJob"/>    <property name="jobDataAsMap">        <map>            <entry key="timeout" value="5"/>        </map>    </property></bean>

这种方式相对复杂点,不过也很简单,jobClass指定任务类,jobDataAsMap可以为任务类属性设值,其灵活度更高。这种方式没有任务并发concurrent属性配置,可以通过@DisallowConcurrentExecution注解实现相同功能,对应example.ComplexJob内容如下

package example;import org.quartz.JobExecutionContext;import org.quartz.JobExecutionException;import org.springframework.scheduling.quartz.QuartzJobBean;import java.util.Date;@DisallowConcurrentExecutionpublic class ComplexJob extends QuartzJobBean {    private int timeout;    /**     * Setter方法在任务初始化后被调用,它的值配置在JobDetailFactoryBean的jobDataAsMap属性中     */    public void setTimeout(int timeout) {        this.timeout = timeout;    }    protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {        System.out.println(new Date() + " : ComplexJob start...");    }}

Job需要继承QuartzJobBean,任务调度时,executeInternal方法将被执行

Step3、配置触发器Trigger

spring也提供了两种方式配置触发器:

方式一:SimpleTriggerFactoryBean

这种方式可以指定任务运行的开始时间、延迟时间、间隔时间等

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">    <!--触发执行任务-->    <property name="jobDetail" ref="simpleJobDetail"/>    <!-- 指定延迟开始,单位毫秒 -->    <property name="startDelay" value="1000"/>    <!-- 重复间隔,单位毫秒 -->    <property name="repeatInterval" value="5000"/></bean>
方式二:CronTriggerFactoryBean

cron表达式,这种方式更加实用(关于cron表达本文就不展开了)

<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">    <property name="jobDetail" ref="complexJobDetail"/>    <!--cron表达式,如每5秒一次-->    <property name="cronExpression" value="0/5 * * * * ?"/></bean>

Step4、配置调度器Scheduler

Scheduler任务调度器,是实际执行任务调度的控制器。可以包含多个Trigger(通过triggers属性配置)和多个Job(通过jobDetails属性配置)。另外提一下,Trigger和Job的关系:一个Trigger包含一个Job,但是一个Job可以被多个Trigger触发。

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">    <property name="triggers">        <list>            <ref bean="cronTrigger"/>        </list>    </property>    <!-- 指定延迟5秒开始 -->    <property name="startupDelay" value="5" />    <property name="schedulerName" value="myScheduler" /></bean>

Step5、测试

package main;import org.springframework.context.support.ClassPathXmlApplicationContext;public class Application {    public static void main(String[] args) {        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring-quartz.xml");        context.registerShutdownHook();    }}

控制台输出:

八月 01, 2017 11:50:50 上午 org.springframework.scheduling.quartz.SchedulerFactoryBean startScheduler信息: Will start Quartz Scheduler [myScheduler] in 5 seconds11:50:51.080 [Timer-0] DEBUG org.quartz.utils.UpdateChecker - Checking for available updated version of Quartz...11:50:55.145 [Quartz Scheduler [myScheduler]] INFO  org.quartz.core.QuartzScheduler - Scheduler myScheduler_$_NON_CLUSTERED started.八月 01, 2017 11:50:55 上午 org.springframework.scheduling.quartz.SchedulerFactoryBean run信息: Starting Quartz Scheduler now, after delay of 5 seconds11:50:55.148 [myScheduler_QuartzSchedulerThread] DEBUG o.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers11:50:55.156 [myScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job DEFAULT.complexJobDetail11:50:55.156 [myScheduler_QuartzSchedulerThread] DEBUG o.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggersTue Aug 01 11:50:55 CST 2017 : ComplexJob start...11:51:00.001 [myScheduler_Worker-2] DEBUG org.quartz.core.JobRunShell - Calling execute on job DEFAULT.complexJobDetail11:51:00.001 [myScheduler_QuartzSchedulerThread] DEBUG o.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggersTue Aug 01 11:51:00 CST 2017 : ComplexJob start...11:51:05.004 [myScheduler_Worker-3] DEBUG org.quartz.core.JobRunShell - Calling execute on job DEFAULT.complexJobDetail11:51:05.004 [myScheduler_QuartzSchedulerThread] DEBUG o.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggersTue Aug 01 11:51:05 CST 2017 : ComplexJob start...

Quartz集群

quartz集群架构图:
这里写图片描述
Quartz 分布式部署时,没有类似zookeeper协调系统来管理各个节点,各个节点并不感知其他节点的存在,只是通过数据库来进行间接的沟通。Quartz 集群容错和负载均衡等集群特性都是通过数据库来实现的。表结构可到官网下载,sql在docs/dbTables目录下,Quartz 数据库表功能说明:

表名 说明 QRTZ_BLOG_TRIGGERS Trigger作为Blob类型存储 QRTZ_CALENDARS 存储Quartz的Calendar信息,Quartz可配置一个日历来指定一个时间范围 QRTZ_CRON_TRIGGERS 存储CronTrigger,包括Cron表达式和时区信息 QRTZ_FIRED_TRIGGERS 存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息 QRTZ_JOB_DETAILS 存储的Job的详细信息,包含[IS_DURABLE]是否持久化、[IS_NONCONCURRENT]是否非并发等信息 QRTZ_LOCKS 存储程序的悲观锁的信息 QRTZ_PAUSED_TRIGGER_GRPS 存储已暂停的Trigger组的信息 QRTZ_SCHEDULER_STATE 存储集群Scheduler实例状态信息 QRTZ_SIMPLE_TRIGGERS 存储简单的Trigger,包括重复次数、间隔、以及已触的次数 QRTZ_TRIGGERS 存储Trigger的信息,包括[START_TIME]开始执行时间,[END_TIME]结束执行时间,[PREV_FIRE_TIME]上次执行时间,[NEXT_FIRE_TIME]下次执行时间,[TRIGGER_TYPE]触发器类型:simple和cron,[TRIGGER_STATE]执行状态:WAITING,PAUSED,ACQUIRED分别为:等待,暂停,运行中

表建好后,通过配置文件配置调度器Scheduler

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">    <!--注册触发器-->    <property name="triggers">        <list>              <ref bean="cronTrigger"/>        </list>    </property>    <property name="configLocation" value="classpath:quartz.properties" />    <!-- 指定延迟5秒开始 -->    <property name="startupDelay" value="5" />    <property name="schedulerName" value="myScheduler" /></bean>

quartz.properties清单:

#============================================================================# Configure Main Scheduler Properties  #============================================================================org.quartz.scheduler.instanceName=myScheduler#集群中每个节点必须有唯一的instanceId,通过org.quartz.spi.InstanceIdGenerator生成org.quartz.scheduler.instanceId=AUTO#是否跳过检查Quartz有新版本可下载,Quartz会启动专门的线程检查更新,发现新版本会打印日志提示org.quartz.scheduler.skipUpdateCheck=true#============================================================================# Configure ThreadPool  #============================================================================org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool#jobs执行可用线程,5个线程差不多可以并发100个短生命周期的任务org.quartz.threadPool.threadCount=5#线程优先级,范围1-10,默认5org.quartz.threadPool.threadPriority=5#============================================================================# Configure JobStore  #============================================================================#线程池中没有可用线程造成触发超时阀值,默认60000毫秒org.quartz.jobStore.misfireThreshold=10000org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTXorg.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegateorg.quartz.jobStore.useProperties=falseorg.quartz.jobStore.dataSource=myDSorg.quartz.jobStore.tablePrefix=QRTZ_org.quartz.jobStore.isClustered=true#集群实例checks-in频率间隔,检测挂掉的实例org.quartz.jobStore.clusterCheckinInterval=10000#============================================================================# Configure DataSource#============================================================================org.quartz.dataSource.myDS.driver=com.mysql.jdbc.Driverorg.quartz.dataSource.myDS.URL=jdbc:mysql://192.168.15.32:3306/quartzorg.quartz.dataSource.myDS.user=rootorg.quartz.dataSource.myDS.password=rootorg.quartz.dataSource.myDS.maxConnections=5org.quartz.dataSource.myDS.validationQuery=select 0

配置好数据源后,设置org.quartz.jobStore.isClustered为true便能获取 Quartz 集群特性

Quartz踩过的坑

问题场景:由于业务需要,使用 Quartz 到各个业务线同步订单数据的,在测试过程中,发现有些订单数据同步正常了,而有些订单数据同步缺少了个别信息,反复检查了代码,未发现问题。

解决:查看QRTZ_SCHEDULER_STATE表,发现订单同步任务相同SCHED_NAME存在多个实例,由于公司的测试环境有多套,不同迭代需求可以在不同测试环境并行,这时候就会有问题:同一个java服务不同环境都启动了,他们有相同的SCHED_NAME,一样的配置,唯一不同的是任务的业务代码不同步,当任务触发负载到代码较旧的实例上执行时,就会造成订单数据同步缺少了个别信息的问题。

吐槽: Quartz 记录的集群实例信息就一个实例名称,通过这个名称完全不知道该实例在哪台机器上,你想停掉对应实例都不知道停哪台机器,要是能记录IP就更好了。

参考

https://docs.spring.io/spring/docs/current/spring-framework-reference/html/scheduling.html

http://www.quartz-scheduler.org

https://tech.meituan.com/mt-crm-quartz.html

http://blog.itpub.net/11627468/viewspace-1763498/

https://www.ibm.com/developerworks/cn/opensource/os-cn-quartz/index.html

https://my.oschina.net/songhongxu/blog/802574

原创粉丝点击