Spring整合Quartz浅析

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;

@Component
public 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;

@DisallowConcurrentExecution
public 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 seconds
11: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 seconds
11:50:55.148 [myScheduler_QuartzSchedulerThread] DEBUG o.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
11:50:55.156 [myScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job DEFAULT.complexJobDetail
11:50:55.156 [myScheduler_QuartzSchedulerThread] DEBUG o.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
Tue 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.complexJobDetail
11:51:00.001 [myScheduler_QuartzSchedulerThread] DEBUG o.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
Tue 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.complexJobDetail
11:51:05.004 [myScheduler_QuartzSchedulerThread] DEBUG o.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
Tue Aug 01 11:51:05 CST 2017 : ComplexJob start...

Quartz集群

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

表名说明
QRTZ_BLOG_TRIGGERSTrigger作为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,默认5
org.quartz.threadPool.threadPriority=5
#============================================================================
# Configure JobStore  
#============================================================================
#线程池中没有可用线程造成触发超时阀值,默认60000毫秒
org.quartz.jobStore.misfireThreshold=10000
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.useProperties=false
org.quartz.jobStore.dataSource=myDS
org.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.Driver
org.quartz.dataSource.myDS.URL=jdbc:mysql://192.168.15.32:3306/quartz
org.quartz.dataSource.myDS.user=root
org.quartz.dataSource.myDS.password=root
org.quartz.dataSource.myDS.maxConnections=5
org.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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值