程序员

任务调度和异步执行器

2018-06-03  本文已影响0人  小螺钉12138

1.任务调度概述

各种企业应用都会遇到任务调度的需求,比如每天凌晨统计论坛用户的积分排名等等,在特定的时间做特定的事情。如果将任务调度的范围稍微扩大一点,则还应该包括资源上的调度。如Web Server在接收到请求时,会立即创建一个新的线程服务该请求。但是资源是有限的,无限制的使用必然会耗尽亏空,大多数系统都要对资源使用进行控制。首先必须限制服务线程的最大数目;其次可以考虑使用线程池以便共享服务的线程资源,降低频繁创建、销毁线程的消耗。

任务调度本身设计多线程并发,运行时间规则制定及解析、运行线程保持与恢复、线程池维护等诸多方面的工作。如果直接使用自定义线程这种最原始的办法,则开发任务调度程序是一项颇具挑战性的工作。

2.Quartz快速进阶

Quartz允许开发人员灵活地定义触发器的调度时间表,并可对触发器和任务进行关联映射。此外,Quartz提供了调度运行环境的持久化机制,可以保存并恢复调度现场,即使系统因故障关闭,任务调度现场数据也不会丢失。

2.1.Quartz基础结构

Quartz对任务调度的领域问题进行了高度的抽象,提出了调度器、任务和触发器这3个核心概念,并且在org.quartz中通过接口和类对核心概念进行了描述

2.2.使用SimpleTrigger

SimpleTrigger拥有多个重载的构造函数,用于在不同场合下构造出对应的实例

public class SimpleJob implements Job {
    public void execute(JobExecutionContext jobCtx) throws JobExecutionException {//实现Job接口方法
        System.out.println(jobCtx.getTrigger().getName()+" triggered. time is:" + (new Date()));
    }
}

以下是通过SimpleTrigger对SimpleJob进行调度

public class SimpleTriggerRunner {
    public static void main(String args[]) {
        try {

            //创建一个JobDetail实例,指定SimpleJob
            JobDetail jobDetail = new JobDetail("job1_1", "jgroup1",
                    SimpleJob.class);
            //通过SimpleTrigger定义调度规则,马上启动,每2秒运行一次,共运行100次
            SimpleTrigger simpleTrigger = new SimpleTrigger("trigger1_1",
                    "tgroup1");
            simpleTrigger.setStartTime(new Date());
            simpleTrigger.setRepeatInterval(2000);
            simpleTrigger.setRepeatCount(100);
            
            //通过SchedulerFactory获取一个调度器实例
            SchedulerFactory schedulerFactory = new StdSchedulerFactory();
            //注册并进行调度
            Scheduler scheduler = schedulerFactory.getScheduler();
            scheduler.scheduleJob(jobDetail, simpleTrigger);
            //调度启动
            scheduler.start();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}
2.3.使用CronTrigger

CronTrigger能够提供比SimpleTrigger更具有实际意义的调度方案,调度规则基于Cron表达式。CronTrigger支持日历相关的周期时间间隔(比如每月第一个周一执行),而不是简单的周期时间间隔。一次相对于SimpleTrigger而言,CronTrigger在使用上也要复杂一些。

2.3.1.Cron表达式

Quartz使用类似Linux下的Cron表达式定义时间规则。Cron表达式由6或7个空格分隔的时间间隔字段组成

位置 时间域名 允许值 允许的特殊字符
1 0-59 ,-*/
2 分钟 0-59 ,-*/
3 小时 0-23 ,-*/
4 日期 1-31 ,-?/L W C
5 月份 1-12 ,-*/
6 星期 1-7 ,-*?/L C #
7 年(可选) 空值 1970-2099 ,-*./
2.3.2.CronTrigger实例

下面使用CronTrigger对SimpleJob进行调度,通过Cron表达式制定调度规则

public class CronTriggerRunner {

    public static void main(String args[]) {
        try {           
            JobDetail jobDetail = new JobDetail("job1_2", "jgroup1",
                    SimpleJob.class);
            CronTrigger cronTrigger = new CronTrigger("trigger1_2", "tgroup1");

            CronExpression cexp = new CronExpression("0/5 * * * * ?");
            cronTrigger.setCronExpression(cexp);
            

            SchedulerFactory schedulerFactory = new StdSchedulerFactory();
            Scheduler scheduler = schedulerFactory.getScheduler();
            scheduler.scheduleJob(jobDetail, cronTrigger);
            scheduler.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.4.使用Calendar

在实际任务调度中,不可能一成不变地按照某个特定周期调度任务,必须考虑到实现生活中日历上的特殊日期

public class CalendarExample {

    public static void main(String[] args) throws Exception {
        SchedulerFactory sf = new StdSchedulerFactory();
        Scheduler scheduler = sf.getScheduler();

        AnnualCalendar holidays = new AnnualCalendar();
        //五一劳动节
        Calendar laborDay = new GregorianCalendar();
        laborDay.add(Calendar.MONTH,5);
        laborDay.add(Calendar.DATE,1);
        holidays.setDayExcluded(laborDay, true);       
        //国庆节
        Calendar nationalDay = new GregorianCalendar();
        nationalDay.add(Calendar.MONTH,10);
        nationalDay.add(Calendar.DATE,1);
        holidays.setDayExcluded(nationalDay, true);


        scheduler.addCalendar("holidays", holidays, false, false);
        
        //从5月1号10am开始
        Date runDate = TriggerUtils.getDateOf(0,0, 10, 1, 5);
        JobDetail job = new JobDetail("job1", "group1", SimpleJob.class);
        SimpleTrigger trigger = new SimpleTrigger("trigger1", "group1", 
                runDate, 
                null, 
                SimpleTrigger.REPEAT_INDEFINITELY, 
                60L * 60L * 1000L);
        //让Trigger遵守节日的规则(排除节日)
        trigger.setCalendarName("holidays");
        scheduler.scheduleJob(job, trigger);
        scheduler.start();
        try {
            // wait 30 seconds to show jobs
            Thread.sleep(30L * 1000L); 
            // executing...
        } catch (Exception e) {
        }            
        scheduler.shutdown(true);
    }
}
2.5.任务调度信息存储

在默认情况下,Quartz将任务调度的运行信息保存在内存中。这种方法提供了最佳性能,因为在内存中数据访问速度最快;不足之处是缺乏数据的持久性,当程序中途停止或者系统崩溃时,所有运行信息都会丢失。

如果确实需要持久化任务调度信息,则Quzrtz允许用户通过调整其属性文件,将这些信息保存到数据库中。在使用数据库保存了任务调度信息后,即使系统崩溃后重新启动,任务调度信息仍将得到恢复。如前面所说的例子,执行50次系统崩溃后重新运行,计数器将从51开始计数。使用数据库保存信息的任务称为持久化任务。

####### 2.5.1.通过配置文件调整任务调度信息的保存策略

其实Quartz JAR文件的org.quartz包下就包含了一个quartz.properties属性配置文件,并提供了默认的配置。如果需要调整配置,则可以在类路径下建立一个新的quartz.properties属性,它将自动被Quartz加载并覆盖默认的配置

//集群的配置,这里不使用集群
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false

//配置调度器的线程池
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

org.quartz.jobStore.misfireThreshold = 60000
//配置任务调度现场保存机制
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

Quartz的属性配置文件主要包括三方面的信息:

可以通过下面的设置将任务调度现场的数据保存到数据库中

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.tablePrefix = QRTZ_ //1、数据库表前缀
org.quartz.jobStore.dataSource = qzDS //2、数据源名称

//3、定义数据源的具体属性
org.quartz.dataSource.qzDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL = jdbc:mysql://localhost:3306/sampledb
org.quartz.dataSource.qzDS.user = root
org.quartz.dataSource.qzDS.password = 123456
org.quartz.dataSource.qzDS.maxConnections = 30

要将任务调度数据保存到数据库中,就必须使用org.quartz.impl.jdbcjobstore.JobStoreTX代替原来的org.quartz.simpl.RAMJobStore,并提供相应的数据库配置信息。首先在1处指定了Quartz数据库表的前缀,然后在2处定义了一个数据源,然后在3处定义这个数据源的连接信息

用户必须事先在相应的数据库中建立Quartz的数据表(共8张),在Quartz的完整发布包的docs/dbTables目录下拥有对应不同数据库的SQL脚本

2.5.2.查询数据库中的运行信息
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true


org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

org.quartz.jobStore.tablePrefix = QRTZ_<!--配置数据库表前缀-->

org.quartz.jobStore.dataSource = qzDS<!--定义数据源名称-->
<!--配置持久化的数据库-->
org.quartz.dataSource.qzDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL = jdbc:mysql://localhost:3306/sampledb
org.quartz.dataSource.qzDS.user = root
org.quartz.dataSource.qzDS.password = 281926
org.quartz.dataSource.qzDS.maxConnections = 30

3.在Spring中使用Quartz

Spring为创建Quartz的Scheduler、Trigger和JobDetail提供了便利的FactoryBean类,以便能够在Spring容器中享受注入的好处。此外,Spring还提供了一些便利工具类,用于直接将Spring中的Bean包装成合法的任务。Spring进一步降低了使用Quartz的难度,能以更具Spring风格的方式使用Quartz。

3.1.创建JobDetail

用户可以直接使用Quartz的JobDetail在Spring中配置一个JobDetail Bean,但是JobDetail使用带参的构造函数,对于习惯通过属性配置的Spring用户来说存在使用上的不便。为此,Spring通过扩展JobDetail提供了一个更具有Bean风格的JobDetailFactoryBean。此外,Spring还提供了一个MethodInvokingJobDetailFactoryBean,通过这个FactoryBean可以将Spring容器中Bean的方法包装成Quartz任务,这样开发者就不必为Job创建对应的类。

3.1.1.JobDetailFactoryBean

JobDetailFactoryBean扩展于Quartz的JobDetail。使用该Bean声明JobDetail时,Bean的名字即任务的名字,如果没有指定所属组,就是用默认组。除了JobDetail中的属性外,还定义了以下属性

配置JobDetail

<bean name="jobDetail" class="org.springframework.scheduling.quartz.JobDetailBean"
        p:jobClass="com.smart.quartz.MyJob"
        p:applicationContextJobDataKey="applicationContext">
        <property name="jobDataAsMap">
            <map>
                <entry key="size" value="10" />
            </map>
        </property>
</bean>

public class MyJob implements StatefulJob {
    public void execute(JobExecutionContext jctx) throws JobExecutionException {
//      Map dataMap = jctx.getJobDetail().getJobDataMap();
        Map dataMap = jctx.getTrigger().getJobDataMap();//获取JobDetail关联的JobDataMap
        String size =(String)dataMap.get("size");
        ApplicationContext ctx = (ApplicationContext)dataMap.get("applicationContext");
        System.out.println("size:"+size);
        dataMap.put("size",size+"0");//对JobDataMap所做的更改是否会被持久化取决于任务的类型
        
        String count =(String)dataMap.get("count");
        System.out.println("count:"+count);
    }
}


3.1.2.MethodInvokingJobDetailFactoryBean

通常情况下,任务都定义在一个业务类方法中,这时,为了满足Quartz Job接口的规
定,还需要定义一个引用业务类方法的实现类。为了避免创建这个只包含一行调用代码的Job实现类,Spring提供了MethodInvokingJobDetailFactoryBean,借由该FactoryBean,可以将一个Bean的某个方法封装成满足Quartz要求的Job

<!-- 通过封装服务类方法实现 -->
    <bean id="jobDetail_1"
        class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"
        p:targetObject-ref="myService" p:targetMethod="doJob" p:concurrent="false" />

    <bean id="myService" class="com.smart.service.MyService" />
    
    
public class MyService {
      public void doJob(){
       System.out.println("in MyService.dojob().");
    }
}

doJob()方法既可以是static的,也可以非static的,但不能拥有方法入参。通过
MethodInvokingJobDetailFactoryBean产生的JobDeatail不能序列化,所以不能被持久化到数据库中。如果希望使用持久化任务,则只能创建正规的Quartz的Job实现类

3.2.创建Trigger
3.2.1.SimpleTriggerFactoryBean

在默认情况下,通过SimpleTriggerFactoryBean配置的Trigger名称即为Bean的名称,属于默认数组。SimpleTriggerFactoryBean在SimpleTrigger的基础上新增了以下属性

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean"
        p:jobDetail-ref="jobDetail" p:startDelay="1000" p:repeatInterval="2000"
        p:repeatCount="100">
        <property name="jobDataAsMap">
            <map>
                <entry key="count" value="10" />
            </map>
        </property>
    </bean>
3.2.2.CronTriggerFactoryBean

CronTriggerFactoryBean扩展于CronTrigger,触发器的名称即为Bean的名称,保存在默认组中。在CronTrigger的基础上,新增的属性和SimpleTriggerFactoryBean大致相同,配置的方法也和SimpleTriggerFactoryBean相似

<bean id="checkImagesTrigger" 
          class="org.springframework.scheduling.quartz.CronTriggerBean"
          p:jobDetail-ref="jobDetail"
          p:cronExpression="0/5 * * * * ?"/>

3.3.创建Scheduler

Quartz的SchedulerFactory是标准的工厂类,不太适合在Spring环境下使用。此外,为了保证Scheduler能够感知Spring容器的生命周期,在Spring容器启动后,Scheduler自动开始工作,而在Spring容器关闭前,自动关闭Scheduler。为此,Spring提供了SchedulerFactoryBean,这个FactoryBean大致拥有以下功能

<bean id="scheduler"
        class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref bean="simpleTrigger" />
            </list>
        </property>
        <property name="schedulerContextAsMap">
            <map>
                <entry key="timeout" value="30" />
            </map>
        </property>
        <property name="quartzProperties">
            <props>
                <prop key="org.quartz.threadPool.class">
                    org.quartz.simpl.SimpleThreadPool
                </prop>
                <prop key="org.quartz.threadPool.threadCount">10</prop>
            </props>
        </property>
    </bean>

在实际应用中,我们并不总是在程序部署的时候就确定需要哪些任务,往往需要在运行期根据业务数据动态产生触发器和任务。用户完全可以在运行时通过代码调用SchedulerFactoryBean获取Scheduler实例,然后动态注册触发器和任务。

上一篇 下一篇

猜你喜欢

热点阅读