美文网首页程序员
任务调度和异步执行器

任务调度和异步执行器

作者: 小螺钉12138 | 来源:发表于2018-06-03 17:33 被阅读0次

    1.任务调度概述

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

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

    2.Quartz快速进阶

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

    2.1.Quartz基础结构

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

    • Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者通
      过实现该接口来定义需要执行的任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息都保存在JobDataMap实例中
    • JobDetail:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不是直接接收一个Job实例,而是接收一个Job实现类,以便运行时通过newInstance()的反射调用机制来实例化Job。因此需要通过一个类来描述Job的实现类及其他相关的静态信息,如Job名称、描述、关联监听器等信息,而JobDetail承担了这一角色
    • Triger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个类。当仅需要触发一次后者以固定间隔周期性执行时,SimpleTigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂的调度方法,
    • Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合
    • Scheduler:代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,二者在Scheduler中拥有各自的组及名称。组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称的组合必须唯一,JobDetail的组合名称的组合也必须唯一。
    • ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程来提高运行效率
    2.2.使用SimpleTrigger

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

    • SimpleTrigger(String name,String group):通过该构造函数指定Trigger所属组合名称
    • SimpleTrigger(String name,String group,Date startTime):除指定Trigger所属组和名
      称外,还可以指定触发的开始时间
    • SimpleTrigger(String name,String group,Date startTime,Date endTime,int repeatCount,long repeatInterval):除指定以上信息外,还可以指定结束时间、重复执行次数、时间间隔等参数
    • SimpleTrigger(String name,String group,String jobName,String jobGroup,Date startTime,Date endTime,int repeatCount,long repeatInterval):这是最复杂的一个构造
      函数,在指定触发参数的同时,通过jobGroup和jobName,使该Trigger和Schedule中的某个任务关联起来
    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 ,-*./
    • 星号(*):可用在所有字段中,表示对应时间域的每一个时刻。例如,*在分钟字段时,表示“每分钟”
    • 问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于占位符
    • 减号(-):表达一个范围,如在小时字段中使用“10-12”,则表示从10点到12点,即10,11,12
    • 逗号(,):表示一个列表值。如在星期字段中使用“MON,WED,FRI”,则表示星期一、星期三和星期五
    • 斜杠(/):x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0,15,30和45秒;而5/15在分钟字段中表示5,20,35,50。用户也可以使用*/y,它等同于0/y
    • L:该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中的意思不同。如果L用在日期字段中,则表示这个月份的最后一天,用在星期中,则表示星期六。但是,如果L出现在星期字段里,而且前面有一个数字N,则表示“这个月的最后N天”。例如,6L表示该月的最后一个星期五
    • W:该字符只能出现在在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如,15W表示离该月15日最近的工作日
    • LW组合:在日期字段中可以组合使用LW,它的意思是当月的最后一个工作日
    • :该字符只能在星期字段中使用,表示当月的某个工作日,如4#5表示当月的第五个星期三。假设当月没有第五个星期三,则忽略不触发

    • C:该字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中的所有日期。例如,5C在日期字段中相当于5日以后的一天,1C在星期字段中相当于星期日后的第一天
    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中的属性外,还定义了以下属性

    • jobClass:类型为Class,实现Job接口的任务类
    • beanName:默认为Bean的id名,通过该属性显示指定Bean名称,它对应任务的名称
    • jobDataAsMap:类型为Map,为任务所对应的JobDataMap提供值。之所以需要提供这个属性,是因为用户无法在Spring的配置文件中为JobDataMap类型的属性提供信息,所以Spring通过jobDataAsMap设置JobDataMap的值
    • applicationContextJobDataKey:用户可以将Spring ApplicationContext的引用保存
      JobDataMap中,以便在Job的代码中访问ApplicationContext.为了达到这个目的,用户需要指定一个键,用于在jobDataAsMap中保存ApplicationContext。如果不设置此键,JobDetailBean就不会将ApplicationContext放入JobDataMap中
    • jobListenerNames:类型为String[],指定注册在Scheduler中的JobListener名称,以便让这些监听器对本任务的时间进行监听

    配置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的基础上新增了以下属性

    • jobDetail:对应的JobDetail
    • beanName:默认为Bean的id名,通过该属性显示指定Bean名称,它对应Trigger的名称
    • jobDataAsMap:以Map类型为Trigger关联的JobDataMap提供值
    • startDelay:延迟多少时间开始触发,单位为毫秒,默认值为0
    • triggerListenerNames:类型为String[],指定注册在Scheduler中的TriggerListener名称,以便让这些监听器对本触发器的事件进行监听
    <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风格的方式为Scheduler提供配置信息
    • 让Scheduler和Spring容器的生命周期建立关联,相生相息
    • 通过属性配置的方式代替Quartz自身的配置文件
    <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实例,然后动态注册触发器和任务。

    相关文章

      网友评论

        本文标题:任务调度和异步执行器

        本文链接:https://www.haomeiwen.com/subject/xrojsftx.html