如何用 java 实现 web 应用中的定时任务? -买球官网平台

1顶
0踩

引用
来源:
作者:

定时任务,是指定一个未来的时间范围执行一定任务的功能。在当前web应用中,多数应用都具备任务调度功能,针对不同的语音,不同的操作系统, 都有其自己的语法及买球软件推荐的解决方案,windows操作系统把它叫做任务计划,linux中cron服务都提供了这个功能,在我们开发业务系统中很多时候会涉及到这个功能。本场chat将使用java语言完成日常开发工作中常用定时任务的使用,希望给大家工作及学习带来帮助。

一、定时任务场景

(1)驱动处理工作流程

作为一个新的预支付订单被初始化放置,如果该订单在指定时间内未进行支付,则将被认为超时订单进行关闭处理;电商系统中应用较多,用户购买商品产生订单,但未进行支付,订单产生30分钟内未支付将关闭订单(且满足该场景数量庞大),不可能采用人工干预。

(2)系统维护

调度工作将获取系统异常日志,及某些关键点数据存储到数据库中,每个工作日(节假日除外平日)在11:30 pm转储到数据库,且生成一个xml文件发送至某位员工邮箱。

(3)在应用程序内提供提醒服务。

系统定时提醒登录用户某时间点执行相关工作。

(4)定时对账任务

公司与三方公司(运营商,银行等)业务,每天零点后进行当天业务的对账,将对账信息结果数据发送至相关负责人邮箱,第二天工作时间进行处理不匹配数据。

(5)数据统计

数据记录较多,实时从数据库读取查询会产生一定时间,为客户体验及性能需要,故每周(天,小时)将数据进行汇总,从而在展示数据时能够快速的呈现数据。

使用定时任务的场景还有很多... 看来定时任务在我们日常的开发中真的应用很广泛...

二、主流定时任务技术讲解

timer

相信大家都已经非常熟悉 java.util.timer 了,它是最简单的一种实现任务调度的方法,下面给出一个具体的例子:
 package com.ibm.scheduler; 
 import java.util.timer; 
 import java.util.timertask; 
 public class timertest extends timertask { 
 private string jobname = ""; 
 public timertest(string jobname) { 
     super(); 
     this.jobname = jobname; 
 } 
 @override 
 public void run() { 
 system.out.println("execute "   jobname); 
 } 
 public static void main(string[] args) { 
     timer timer = new timer(); 
     long delay1 = 1 * 1000; 
     long period1 = 1000; 
     // 从现在开始 1 秒钟之后,每隔 1 秒钟执行一次 job1 
     timer.schedule(new timertest("job1"), delay1, period1); 
     long delay2 = 2 * 1000; 
     long period2 = 2000; 
     // 从现在开始 2 秒钟之后,每隔 2 秒钟执行一次 job2 
     timer.schedule(new timertest("job2"), delay2, period2); 
     } 
 } 

  /**
输出结果: 
execute job1 
execute job1 
execute job2 
execute job1 
execute job1 
execute job2 
*/

使用 timer 实现任务调度的核心类是 timer 和 timertask。其中 timer 负责设定 timertask 的起始与间隔执行时间。使用者只需要创建一个 timertask 的继承类,实现自己的 run 方法,然后将其丢给 timer 去执行即可。timer 的设计核心是一个 tasklist 和一个 taskthread。timer 将接收到的任务丢到自己的 tasklist 中,tasklist 按照 task 的最初执行时间进行排序。timerthread 在创建 timer 时会启动成为一个守护线程。这个线程会轮询所有任务,找到一个最近要执行的任务,然后休眠,当到达最近要执行任务的开始时间点,timerthread 被唤醒并执行该任务。之后 timerthread 更新最近一个要执行的任务,继续休眠。

timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(这点需要注意)。

scheduledexecutor

鉴于 timer 的上述缺陷,java 5 推出了基于线程池设计的 scheduledexecutor。其设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。需 要注意的是,只有当任务的执行时间到来时,scheduedexecutor 才会真正启动一个线程,其余时间 scheduledexecutor 都是在轮询任务的状态。
package com.ibm.scheduler;
import java.util.concurrent.executors;
import java.util.concurrent.scheduledexecutorservice;
import java.util.concurrent.timeunit;
public class scheduledexecutortest implements runnable {
    private string jobname = "";
    public scheduledexecutortest(string jobname) {
        super();
        this.jobname = jobname;
    }
    @override
    public void run() {
        system.out.println("execute "   jobname);
    }
    public static void main(string[] args) {
        scheduledexecutorservice service = executors.newscheduledthreadpool(10);
        long initialdelay1 = 1;
        long period1 = 1;
        // 从现在开始1秒钟之后,每隔1秒钟执行一次job1
        service.scheduleatfixedrate(
                new scheduledexecutortest("job1"), initialdelay1,
                period1, timeunit.seconds);
        long initialdelay2 = 1;
        long delay2 = 1;
        // 从现在开始2秒钟之后,每隔2秒钟执行一次job2
        service.schedulewithfixeddelay(
                new scheduledexecutortest("job2"), initialdelay2,
                delay2, timeunit.seconds);
    }
}

/**
输出结果:
execute job1
execute job1
execute job2
execute job1
execute job1
execute job2
*/

上述代码展示了 scheduledexecutorservice 中两种最常用的调度方法 scheduleatfixedrate 和 schedulewithfixeddelay。scheduleatfixedrate 每次执行时间为上一次任务开始起向后推一个时间间隔,即每次执行时间为 :initialdelay, initialdelay period, initialdelay 2*period, … schedulewithfixeddelay每次执行时间为上一次任务结束起向后推一个时间间隔,即每次执行时间为:initialdelay, initialdelay executetime delay, initialdelay 2*executetime 2*delay。由此可见,scheduleatfixedrate 是基于固定时间间隔进行任务调度,schedulewithfixeddelay 取决于每次任务执行的时间长短,是基于不固定时间间隔进行任务调度。

用 scheduledexecutor 和 calendar 实现复杂任务调度

timer 和 scheduledexecutor 都仅能提供基于开始时间与重复间隔的任务调度,不能胜任更加复杂的调度需求。比如,设置每星期二的 16:38:10 执行任务。该功能使用 timer 和 scheduledexecutor 都不能直接实现,但我们可以借助 calendar 间接实现该功能。
package com.ibm.scheduler;
import java.util.calendar;
import java.util.date;
import java.util.timertask;
import java.util.concurrent.executors;
import java.util.concurrent.scheduledexecutorservice;
import java.util.concurrent.timeunit;
public class scheduledexceutortest2 extends timertask {
    private string jobname = "";
    public scheduledexceutortest2(string jobname) {
        super();
        this.jobname = jobname;
    }
    @override
    public void run() {
        system.out.println("date = " new date() ", execute "   jobname);
    }
    /**
     * 计算从当前时间currentdate开始,满足条件dayofweek, hourofday, 
     * minuteofhour, secondofminite的最近时间
     * @return
     */
    public calendar getearliestdate(calendar currentdate, int dayofweek,
            int hourofday, int minuteofhour, int secondofminite) {
        //计算当前时间的week_of_year,day_of_week, hour_of_day, minute,second等各个字段值
        int currentweekofyear = currentdate.get(calendar.week_of_year);
        int currentdayofweek = currentdate.get(calendar.day_of_week);
        int currenthour = currentdate.get(calendar.hour_of_day);
        int currentminute = currentdate.get(calendar.minute);
        int currentsecond = currentdate.get(calendar.second);
        //如果输入条件中的dayofweek小于当前日期的dayofweek,则week_of_year需要推迟一周
        boolean weeklater = false;
        if (dayofweek < currentdayofweek) {
            weeklater = true;
        } else if (dayofweek == currentdayofweek) {
            //当输入条件与当前日期的dayofweek相等时,如果输入条件中的
            //hourofday小于当前日期的
            //currenthour,则week_of_year需要推迟一周    
            if (hourofday < currenthour) {
                weeklater = true;
            } else if (hourofday == currenthour) {
                 //当输入条件与当前日期的dayofweek, hourofday相等时,
                 //如果输入条件中的minuteofhour小于当前日期的
                //currentminute,则week_of_year需要推迟一周
                if (minuteofhour < currentminute) {
                    weeklater = true;
                } else if (minuteofhour == currentsecond) {
                     //当输入条件与当前日期的dayofweek, hourofday, 
                     //minuteofhour相等时,如果输入条件中的
                    //secondofminite小于当前日期的currentsecond,
                    //则week_of_year需要推迟一周
                    if (secondofminite < currentsecond) {
                        weeklater = true;
                    }
                }
            }
        }
        if (weeklater) {
            //设置当前日期中的week_of_year为当前周推迟一周
            currentdate.set(calendar.week_of_year, currentweekofyear   1);
        }
        // 设置当前日期中的day_of_week,hour_of_day,minute,second为输入条件中的值。
        currentdate.set(calendar.day_of_week, dayofweek);
        currentdate.set(calendar.hour_of_day, hourofday);
        currentdate.set(calendar.minute, minuteofhour);
        currentdate.set(calendar.second, secondofminite);
        return currentdate;
    }
    public static void main(string[] args) throws exception {
        scheduledexceutortest2 test = new scheduledexceutortest2("job1");
        //获取当前时间
        calendar currentdate = calendar.getinstance();
        long currentdatelong = currentdate.gettime().gettime();
        system.out.println("current date = "   currentdate.gettime().tostring());
        //计算满足条件的最近一次执行时间
        calendar earliestdate = test
                .getearliestdate(currentdate, 3, 16, 38, 10);
        long earliestdatelong = earliestdate.gettime().gettime();
        system.out.println("earliest date = "
                  earliestdate.gettime().tostring());
        //计算从当前时间到最近一次执行时间的时间间隔
        long delay = earliestdatelong - currentdatelong;
        //计算执行周期为一星期
        long period = 7 * 24 * 60 * 60 * 1000;
        scheduledexecutorservice service = executors.newscheduledthreadpool(10);
        //从现在开始delay毫秒之后,每隔一星期执行一次job1
        service.scheduleatfixedrate(test, delay, period,
                timeunit.milliseconds);
    }
} 

/**
输出结果:
current date = wed feb 02 17:32:01 cst 2011
earliest date = tue feb 8 16:38:10 cst 2011
date = tue feb 8 16:38:10 cst 2011, execute job1
date = tue feb 15 16:38:10 cst 2011, execute job1
*/

上述代码实现了每星期二 16:38:10 调度任务的功能。其核心在于根据当前时间推算出最近一个星期二 16:38:10 的绝对时间,然后计算与当前时间的时间差,作为调用 scheduledexceutor 函数的参数。计算最近时间要用到 java.util.calendar 的功能。首先需要解释 calendar 的一些设计思想。calendar 有以下几种唯一标识一个日期的组合方式:
引用
year month day_of_month
year month week_of_month day_of_week
year month day_of_week_in_month day_of_week
year day_of_year
year day_of_week week_of_year

上述组合分别加上 hourofday minute second 即为一个完整的时间标识。

上述demo采用了最后一种组合方式。输入为 day_of_week, hour_of_day, minute, second 以及当前日期 , 输出为一个满足 day_of_week, hour_of_day, minute, second 并且距离当前日期最近的未来日期。计算的原则是从输入的 day_of_week 开始比较,如果小于当前日期的 day_of_week,则需要向 week_of_year 进一, 即将当前日期中的 week_of_year 加一并覆盖旧值;如果等于当前的 day_of_week, 则继续比较 hour_of_day;如果大于当前的 day_of_week,则直接调用 java.util.calenda 的 calendar.set(field, value) 函数将当前日期的 day_of_week, hour_of_day, minute, second 赋值为输入值,依次类推,直到比较至 second。我们可以根据输入需求选择不同的组合方式来计算最近执行时间。

用上述方法实现该任务调度比较繁琐,期待需要一个更加完善的任务调度工具来解决这些复杂的调度问题。幸运的是,开源工具包 quartz 在这方面展现了强大的能力。

quartz

opensymphony开源组织在job scheduling领域又一个开源项目,它可以与j2ee与j2se应用程序相结合也可以单独使用。quartz可以用来创建简单或为运行十个,百个,甚至是好几万个jobs这样复杂的程序。

先来看一个例子吧:
package com.test.quartz;
import static org.quartz.datebuilder.newdate;
import static org.quartz.jobbuilder.newjob;
import static org.quartz.simpleschedulebuilder.simpleschedule;
import static org.quartz.triggerbuilder.newtrigger;
import java.util.gregoriancalendar;
import org.quartz.jobdetail;
import org.quartz.scheduler;
import org.quartz.trigger;
import org.quartz.impl.stdschedulerfactory;
import org.quartz.impl.calendar.annualcalendar;
public class quartztest {
    public static void main(string[] args) {
        try {
            //创建scheduler
            scheduler scheduler = stdschedulerfactory.getdefaultscheduler();
            //定义一个trigger
            trigger trigger = newtrigger().withidentity("trigger1", "group1") //定义name/group
                .startnow()//一旦加入scheduler,立即生效
                .withschedule(simpleschedule() //使用simpletrigger
                    .withintervalinseconds(1) //每隔一秒执行一次
                    .repeatforever()) //一直执行,奔腾到老不停歇
                .build();
            //定义一个jobdetail
            jobdetail job = newjob(helloquartz.class) //定义job类为helloquartz类,这是真正的执行逻辑所在
                .withidentity("job1", "group1") //定义name/group
                .usingjobdata("name", "quartz") //定义属性
                .build();
            //加入这个调度
            scheduler.schedulejob(job, trigger);
            //启动之
            scheduler.start();
            //运行一段时间后关闭
            thread.sleep(10000);
            scheduler.shutdown(true);
        } catch (exception e) {
            e.printstacktrace();
        }
    }
}
package com.test.quartz;
import java.util.date;
import org.quartz.disallowconcurrentexecution;
import org.quartz.job;
import org.quartz.jobdetail;
import org.quartz.jobexecutioncontext;
import org.quartz.jobexecutionexception;
public class helloquartz implements job {
    public void execute(jobexecutioncontext context) throws jobexecutionexception {
        jobdetail detail = context.getjobdetail();
        string name = detail.getjobdatamap().getstring("name");
        system.out.println("say hello to "   name   " at "   new date());
    }
}

通过以上例子:quartz最重要的3个基本要素:
  • scheduler:调度器。所有的调度都是由它控制。
  • trigger: 定义触发的条件。例子中,它的类型是simpletrigger,每隔1秒中执行一次(什么是simpletrigger下面会有详述)。
  • jobdetail & job: jobdetail 定义的是任务数据,而真正的执行逻辑是在job中,例子中是helloquartz。 为什么设计成jobdetail job,不直接使用job?这是因为任务是有可能并发执行,如果scheduler直接使用job,就会存在对同一个job实例并发访问的问题。而jobdetail & job 方式,sheduler每次执行,都会根据jobdetail创建一个新的job实例,这样就可以规避并发访问的问题。
quartz api

quartz的api的风格在2.x以后,采用的是dsl风格(通常意味着fluent interface风格),就是示例中newtrigger()那一段东西。它是通过builder实现的,就是以下几个。(下面大部分代码都要引用这些builder )
//job相关的builder
import static org.quartz.jobbuilder.*;
//trigger相关的builder
import static org.quartz.triggerbuilder.*;
import static org.quartz.simpleschedulebuilder.*;
import static org.quartz.cronschedulebuilder.*;
import static org.quartz.dailytimeintervalschedulebuilder.*;
import static org.quartz.calendarintervalschedulebuilder.*;
//日期相关的builder
import static org.quartz.datebuilder.*;

dsl风格写起来会更加连贯,畅快,而且由于不是使用setter的风格,语义上会更容易理解一些。对比一下:
jobdetail jobdetail=new jobdetailimpl("jobdetail1","group1",helloquartz.class);
jobdetail.getjobdatamap().put("name", "quartz");
simpletriggerimpl trigger=new simpletriggerimpl("trigger1","group1");
trigger.setstarttime(new date());
trigger.setrepeatinterval(1);
trigger.setrepeatcount(-1);


关于name和group

jobdetail和trigger都有name和group。

name是它们在这个sheduler里面的唯一标识。如果我们要更新一个jobdetail定义,只需要设置一个name相同的jobdetail实例即可。

group是一个组织单元,sheduler会提供一些对整组操作的api,比如 scheduler.resumejobs()。

trigger

在开始详解每一种trigger之前,需要先了解一下trigger的一些共性。

starttime & endtime

starttime和endtime指定的trigger会被触发的时间区间。在这个区间之外,trigger是不会被触发的。 所有trigger都会包含这两个属性。

优先级(priority)

当scheduler比较繁忙的时候,可能在同一个时刻,有多个trigger被触发了,但资源不足(比如线程池不足)。那么这个时候比剪刀石头布更好的方式,就是设置优先级。优先级高的先执行。 需要注意的是,优先级只有在同一时刻执行的trigger之间才会起作用,如果一个trigger是9:00,另一个trigger是9:30。那么无论后一个优先级多高,前一个都是先执行。 优先级的值默认是5,当为负数时使用默认值。最大值似乎没有指定,但建议遵循java的标准,使用1-10,不然鬼才知道看到【优先级为10】是时,上头还有没有更大的值。

misfire(错失触发)策略

类似的scheduler资源不足的时候,或者机器崩溃重启等,有可能某一些trigger在应该触发的时间点没有被触发,也就是miss fire了。这个时候trigger需要一个策略来处理这种情况。每种trigger可选的策略各不相同。这里有两个点需要重点注意:

misfire的触发是有一个阀值,这个阀值是配置在jobstore的。比ramjobstore是org.quartz.jobstore.misfirethreshold。只有超过这个阀值,才会算misfire。小于这个阀值,quartz是会全部重新触发。所有misfire的策略实际上都是解答两个问题:
  • 已经misfire的任务还要重新触发吗?
  • 如果发生misfire,要调整现有的调度时间吗?
比如simpletrigger的misfire策略有:
  • misfire_instruction_ignore_misfire_policy 这个不是忽略已经错失的触发的意思,而是说忽略misfire策略。它会在资源合适的时候,重新触发所有的misfire任务,并且不会影响现有的调度时间。比如,simpletrigger每15秒执行一次,而中间有5分钟时间它都misfire了,一共错失了20个,5分钟后,假设资源充足了,并且任务允许并发,它会被一次性触发。这个属性是所有trigger都适用。
  • misfire_instruction_fire_now 忽略已经misfire的任务,并且立即执行调度。这通常只适用于只执行一次的任务。
  • misfire_instruction_reschedule_now_with_existing_repeat_count 将starttime设置当前时间,立即重新调度任务,包括的misfire的。
  • misfire_instruction_reschedule_now_with_remaining_repeat_count 类似misfireinstructionreschedulenowwithexistingrepeat_count,区别在于会忽略已经misfire的任务。
  • misfire_instruction_reschedule_next_with_existing_count 在下一次调度时间点,重新开始调度任务,包括的misfire的。
  • misfire_instruction_reschedule_next_with_remaining_count 类似于misfireinstructionreschedulenextwithexistingcount,区别在于会忽略已经misfire的任务。
  • misfire_instruction_smart_policy 所有的trigger的misfire默认值都是这个,大致意思是“把处理逻辑交给聪明的quartz去决定”。基本策略是。
  • 如果是只执行一次的调度,使用misfire_instruction_fire_now。
  • 如果是无限次的调度(repeatcount是无限的),使用misfire_instruction_reschedule_next_with_remaining_count。
  • 否则,使用misfire_instruction_reschedule_now_with_existing_repeat_count misfire的东西挺繁杂的,可以参考这篇。
calendar

这里的calendar不是jdk的java.util.calendar,不是为了计算日期的。它的作用是在于补充trigger的时间。可以排除或加入某一些特定的时间点。

以”每月25日零点自动还卡债“为例,我们想排除掉每年的2月25号零点这个时间点(因为有2.14,所以2月一定会破产)。这个时间,就可以用calendar来实现。

例子:
annualcalendar cal = new annualcalendar(); //定义一个每年执行calendar,精度为天,即不能定义到2.25号下午2:00
java.util.calendar excludeday = new gregoriancalendar();
excludeday.settime(newdate().inmonthonday(2, 25).build());
cal.setdayexcluded(excludeday, true);  //设置排除2.25这个日期
scheduler.addcalendar("febcal", cal, false, false); //scheduler加入这个calendar
//定义一个trigger
trigger trigger = newtrigger().withidentity("trigger1", "group1") 
    .startnow()//一旦加入scheduler,立即生效
    .modifiedbycalendar("febcal") //使用calendar !!
    .withschedule(simpleschedule()
        .withintervalinseconds(1) 
        .repeatforever()) 
    .build();

quartz体贴地为我们提供以下几种calendar,注意,所有的calendar既可以是排除,也可以是包含,取决于:
  • holidaycalendar。指定特定的日期,比如20140613。精度到天。
  • dailycalendar。指定每天的时间段(rangestartingtime, rangeendingtime),格式是hh:mm[:ss[:mmm]]。也就是最大精度可以到毫秒。
  • weeklycalendar。指定每星期的星期几,可选值比如为java.util.calendar.sunday。精度是天。
  • monthlycalendar。指定每月的几号。可选值为1-31。精度是天
  • annualcalendar。 指定每年的哪一天。使用方式如上例。精度是天。
  • croncalendar。指定cron表达式。精度取决于cron表达式,也就是最大精度可以到秒。

trigger实现类

quartz有以下几种trigger实现:

simpletrigger

指定从某一个时间开始,以一定的时间间隔(单位是毫秒)执行的任务。它适合的任务类似于:9:00 开始,每隔1小时,执行一次。它的属性有:
  • repeatinterval 重复间隔
  • repeatcount 重复次数。实际执行次数是 repeatcount 1。因为在starttime的时候一定会执行一次。下面有关repeatcount 属性的都是同理。
例子:
simpleschedule()
        .withintervalinhours(1) //每小时执行一次
        .repeatforever() //次数不限
        .build();
simpleschedule()
    .withintervalinminutes(1) //每分钟执行一次
    .withrepeatcount(10) //次数为10次
    .build();

calendarintervaltrigger

类似于simpletrigger,指定从某一个时间开始,以一定的时间间隔执行的任务。 但是不同的是simpletrigger指定的时间间隔为毫秒,没办法指定每隔一个月执行一次(每月的时间间隔不是固定值),而calendarintervaltrigger支持的间隔单位有秒,分钟,小时,天,月,年,星期。 相较于simpletrigger有两个优势:1、更方便,比如每隔1小时执行,你不用自己去计算1小时等于多少毫秒。 2、支持不是固定长度的间隔,比如间隔为月和年。但劣势是精度只能到秒。它适合的任务类似于:9:00 开始执行,并且以后每周 9:00 执行一次。它的属性有:
  • interval 执行间隔
  • intervalunit 执行间隔的单位(秒,分钟,小时,天,月,年,星期)
例子:
calendarintervalschedule()
    .withintervalindays(1) //每天执行一次
    .build();
calendarintervalschedule()
    .withintervalinweeks(1) //每周执行一次
    .build();

dailytimeintervaltrigger

指定每天的某个时间段内,以一定的时间间隔执行任务。并且它可以支持指定星期。它适合的任务类似于:指定每天9:00 至 18:00 ,每隔70秒执行一次,并且只要周一至周五执行。 它的属性有:
  • starttimeofday 每天开始时间
  • endtimeofday 每天结束时间
  • daysofweek 需要执行的星期
  • interval 执行间隔
  • intervalunit 执行间隔的单位(秒,分钟,小时,天,月,年,星期)
  • repeatcount 重复次数
例子:
dailytimeintervalschedule()
    .startingdailyat(timeofday.hourandminuteofday(9, 0)) //第天9:00开始
    .endingdailyat(timeofday.hourandminuteofday(16, 0)) //16:00 结束 
    .ondaysoftheweek(monday,tuesday,wednesday,thursday,friday) //周一至周五执行
    .withintervalinhours(1) //每间隔1小时执行一次
    .withrepeatcount(100) //最多重复100次(实际执行100 1次)
    .build();
dailytimeintervalschedule()
    .startingdailyat(timeofday.hourandminuteofday(9, 0)) //第天9:00开始
    .endingdailyaftercount(10) //每天执行10次,这个方法实际上根据 starttimeofday interval*count 算出 endtimeofday
    .ondaysoftheweek(monday,tuesday,wednesday,thursday,friday) //周一至周五执行
    .withintervalinhours(1) //每间隔1小时执行一次
    .build();

crontrigger
适合于更复杂的任务,它支持类型于linux cron的语法(并且更强大)。基本上它覆盖了以上三个trigger的绝大部分能力(但不是全部)—— 当然,也更难理解。它适合的任务类似于:每天0:00,9:00,18:00各执行一次。它的属性只有:

cron表达式

但这个表示式本身就够复杂了。下面会有说明。例子:
cronschedule("0 0/2 8-17 * * ?") // 每天8:00-17:00,每隔2分钟执行一次
    .build();
cronschedule("0 30 9 ? * mon") // 每周一,9:30执行一次
.build();
weeklyondayandhourandminute(monday,9, 30) //等同于 0 30 9 ? * mon 
    .build();

cron表达式
位置 时间域 允许值 特殊值
1 0-59 , - * /
2 分钟 0-59 , - * /
3 小时 0-23 , - * /
4 日期 1-31 , - * ? / l w c
5 月份 1-12 , - * /
6 星期 1-7 , - * ? / l c #
7 年份(可选) 1-31 , - * /

  • 星号():可用在所有字段中,表示对应时间域的每一个时刻,例如, 在分钟字段时,表示“每分钟”;
  • 问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符;
  • 减号(-):表达一个范围,如在小时字段中使用“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在日期字段中,表示这个月份的最后一天,如一月的31号,非闰年二月的28号;如果l用在星期中,则表示星期六,等同于7。但是,如果l出现在星期字段里,而且在前面有一个数值x,则表示“这个月的最后x天”,例如,6l表示该月的最后星期五;
  • w:该字符只能出现在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如15w表示离该月15号最近的工作日,如果该月15号是星期六,则匹配14号星期五;如果15日是星期日,则匹配16号星期一;如果15号是星期二,那结果就是15号星期二。但必须注意关联的匹配日期不能够跨月,如你指定1w,如果1号是星期六,结果匹配的是3号星期一,而非上个月最后的那天。w字符串只能指定单一日期,而不能指定日期范围;
  • lw组合:在日期字段可以组合使用lw,它的意思是当月的最后一个工作日; 井号(#):该字符只能在星期字段中使用,表示当月某个工作日。如6#3表示当月的第三个星期五(6表示星期五,#3表示当前的第三个),而4#5表示当月的第五个星期三,假设当月没有第五个星期三,忽略不触发;
  • c:该字符只在日期和星期字段中使用,代表“calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如5c在日期字段中就相当于日历5日以后的第一天。1c在星期字段中相当于星期日后的第一天。
cron表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。一些例子:
表示式 说明
0 0 12 * * ? 每天12点运行
0 15 10 ? * * 每天10:15运行
0 15 10 * * ? 每天10:15运行
0 15 10 * * ? * 每天10:15运行
0 15 10 * * ? 2008 在2008年的每天10:15运行
0 * 14 * * ? 每天14点到15点之间每分钟运行一次,开始于14:00,结束于14:59。
0 0/5 14 * * ? 每天14点到15点每5分钟运行一次,开始于14:00,结束于14:55。
0 0/5 14,18 * * ? 每天14点到15点每5分钟运行一次,此外每天18点到19点每5钟也运行一次。
0 0-5 14 * * ? 每天14:00点到14:05,每分钟运行一次。
0 10,44 14 ? 3 wed 3月每周三的14:10分到14:44,每分钟运行一次。
0 15 10 ? * mon-fri 每周一,二,三,四,五的10:15分运行。
0 15 10 15 * ? 每月15日10:15分运行。
0 15 10 l * ? 每月最后一天10:15分运行。
0 15 10 ? * 6l 每月最后一个星期五10:15分运行。
0 15 10 ? * 6l 2007-2009 在2007,2008,2009年每个月的最后一个星期五的10:15分运行。
0 15 10 ? * 6#3 每月第三个星期五的10:15分运行。

jobdetail & job

jobdetail是任务的定义,而job是任务的执行逻辑。在jobdetail里会引用一个job class定义。一个最简单的例子:
public class jobtest {
    public static void main(string[] args) throws schedulerexception, ioexception {
           jobdetail job=newjob()
               .oftype(donothingjob.class) //引用job class
               .withidentity("job1", "group1") //设置name/group
               .withdescription("this is a test job") //设置描述
               .usingjobdata("age", 18) //加入属性到agejobdatamap
               .build();
           job.getjobdatamap().put("name", "quertz"); //加入属性name到jobdatamap
           //定义一个每秒执行一次的simpletrigger
           trigger trigger=newtrigger()
                   .startnow()
                   .withidentity("trigger1")
                   .withschedule(simpleschedule()
                       .withintervalinseconds(1)
                       .repeatforever())
                   .build();
           scheduler sche=stdschedulerfactory.getdefaultscheduler();
           sche.schedulejob(job, trigger);
           sche.start();
           system.in.read();
           sche.shutdown();
    }
}
public class donothingjob implements job {
    public void execute(jobexecutioncontext context) throws jobexecutionexception {
        system.out.println("do nothing");
    }
}

从上例我们可以看出,要定义一个任务,需要干几件事:
  • 创建一个org.quartz.job的实现类,并实现实现自己的业务逻辑。比如上面的donothingjob。
  • 定义一个jobdetail,引用这个实现类
  • 加入schedulejob quartz调度一次任务,会干如下的事:
  • jobclass jobclass=jobdetail.getjobclass()
  • job jobinstance=jobclass.newinstance()。所以job实现类,必须有一个public的无参构建方法。
  • jobinstance.execute(jobexecutioncontext context)。jobexecutioncontext是job运行的上下文,可以获得trigger、scheduler、jobdetail的信息。
也就是说,每次调度都会创建一个新的job实例,这样的好处是有些任务并发执行的时候,不存在对临界资源的访问问题——当然,如果需要共享jobdatamap的时候,还是存在临界资源的并发访问的问题。

jobdatamap

job是newinstance的实例,那我怎么传值给它? 比如我现在有两个发送邮件的任务,一个是发给"lilei",一个发给"hanmeimei",不能说我要写两个job实现类lileisendemailjob和hanmeimeisendemailjob。实现的办法是通过jobdatamap。

每一个jobdetail都会有一个jobdatamap。jobdatamap本质就是一个map的扩展类,只是提供了一些更便捷的方法,比如getstring()之类的。

我们可以在定义jobdetail,加入属性值,方式有二:
  • newjob().usingjobdata("age", 18) //加入属性到agejobdatamap
  • job.getjobdatamap().put("name", "quertz"); //加入属性name到jobdatamap
然后在job中可以获取这个jobdatamap的值,方式同样有二:
public class helloquartz implements job {
    private string name;
    public void execute(jobexecutioncontext context) throws jobexecutionexception {
        jobdetail detail = context.getjobdetail();
        jobdatamap map = detail.getjobdatamap(); //方法一:获得jobdatamap
        system.out.println("say hello to "   name   "["   map.getint("age")   "]"   " at "
                             new date());
    }
  //方法二:属性的setter方法,会将jobdatamap的属性自动注入
    public void setname(string name) { 
        this.name = name;
    }
}

对于同一个jobdetail实例,执行的多个job实例,是共享同样的jobdatamap,也就是说,如果你在任务里修改了里面的值,会对其他job实例(并发的或者后续的)造成影响。

除了jobdetail,trigger同样有一个jobdatamap,共享范围是所有使用这个trigger的job实例。

job并发

job是有可能并发执行的,比如一个任务要执行10秒中,而调度算法是每秒中触发1次,那么就有可能多个任务被并发执行。

有时候我们并不想任务并发执行,比如这个任务要去”获得数据库中所有未发送邮件的名单“,如果是并发执行,就需要一个数据库锁去避免一个数据被多次处理。这个时候一个@disallowconcurrentexecution解决这个问题。就是这样:
public class donothingjob implements job {
    @disallowconcurrentexecution
    public void execute(jobexecutioncontext context) throws jobexecutionexception {
        system.out.println("do nothing");
    }
}

注意,@disallowconcurrentexecution是对jobdetail实例生效,也就是如果你定义两个jobdetail,引用同一个job类,是可以并发执行的。

jobexecutionexception

job.execute()方法是不允许抛出除jobexecutionexception之外的所有异常的(包括runtimeexception),所以编码的时候,最好是try-catch住所有的throwable,小心处理。

其他属性

  • durability(耐久性?) 如果一个任务不是durable,那么当没有trigger关联它的时候,它就会被自动删除。
  • requestsrecovery 如果一个任务是"requests recovery",那么当任务运行过程非正常退出时(比如进程崩溃,机器断电,但不包括抛出异常这种情况),quartz再次启动时,会重新运行一次这个任务实例。
可以通过jobexecutioncontext.isrecovering()查询任务是否是被恢复的。

scheduler

  • scheduler就是quartz的大脑,所有任务都是由它来设施。
  • schduelr包含一个两个重要组件: jobstore和threadpool。
  • jobstore是会来存储运行时信息的,包括trigger,schduler,jobdetail,业务锁等。它有多种实现ramjob(内存实现),jobstoretx(jdbc,事务由quartz管理),jobstorecmt(jdbc,使用容器事务),clusteredjobstore(集群实现)、terracottajobstore(什么是terractta)。
  • threadpool就是线程池,quartz有自己的线程池实现。所有任务的都会由线程池执行。

schedulerfactory

schdulerfactory,顾名思义就是来用创建schduler了,有两个实现:directschedulerfactory和 stdschdulerfactory。前者可以用来在代码里定制你自己的schduler参数。后者是直接读取classpath下的quartz.properties(不存在就都使用默认值)配置来实例化schduler。通常来讲,我们使用stdschdulerfactory也就足够了。

schdulerfactory本身是支持创建rmi stub的,可以用来管理远程的scheduler,功能与本地一样,可以远程提交个job什么的。directschedulerfactory的创建接口:
    /**
     * same as
     * {@link directschedulerfactory#createscheduler(threadpool threadpool, jobstore jobstore)},
     * with the addition of specifying the scheduler name and instance id. this
     * scheduler can only be retrieved via
     * {@link directschedulerfactory#getscheduler(string)}
     *
     * @param schedulername
     *          the name for the scheduler.
     * @param schedulerinstanceid
     *          the instance id for the scheduler.
     * @param threadpool
     *          the thread pool for executing jobs
     * @param jobstore
     *          the type of job store
     * @throws schedulerexception
     *           if initialization failed
     */
     public void createscheduler(string schedulername,
                string schedulerinstanceid, threadpool threadpool, jobstore jobstore)
            throws schedulerexception;

stdschdulerfactory的配置例子, 更多配置,参考quartz配置指南:
org.quartz.scheduler.instancename = defaultquartzscheduler
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.simpl.ramjobstore

三、quartz 集成 spring

开发一个job类,普通java类,需要有一个执行的方法:

package com.tgb.lk.demo.quartz;
import java.util.date;
public class myjob {
        public void work() {
            system.out.println("date:"   new date().tostring());
        }
}

把类放到spring容器中,可以使用配置也可以使用注解:

配置jobdetail,指定job对象:
    
        
            
        
        
            work
        
    

配置一个trigger,需要指定一个cron表达式,指定任务的执行时机:
  
        
            
                
            
            
                0/3 * * * * ?
            
        

配置调度工厂:
    
        
            
                
            
        
    
    

项目启动,定时器开始执行。

四、分析不同定时任务优缺点,寻找一种符合你项目需求的定时任务

timer管理延时任务的缺陷

以前在项目中也经常使用定时器,比如每隔一段时间清理项目中的一些垃圾文件,每隔一段时间进行日志清理;然而timer是存在一些缺陷的,因为timer在执行定时任务时只会创建一个线程,所以如果存在多个任务,且任务时间过长,超过了两个任务的间隔时间,会发生一些缺陷

timer当任务抛出异常时的缺陷

如果timertask抛出runtimeexception,timer会停止所有任务的运行

timer执行周期任务时依赖系统时间

timer执行周期任务时依赖系统时间,如果当前系统时间发生变化会出现一些执行上的变化,scheduledexecutorservice基于时间的延迟,不会由于系统时间的改变发生执行变化。

对异常的处理

quartz的某次执行任务过程中抛出异常,不影响下一次任务的执行,当下一次执行时间到来时,定时器会再次执行任务;而timertask则不同,一旦某个任务在执行过程中抛出异常,则整个定时器生命周期就结束,以后永远不会再执行定时器任务。

精确到和功能

quartz每次执行任务都创建一个新的任务类对象,而timertask则每次使用同一个任务类对象。 quartz可以通过cron表达式精确到特定时间执行,而timertask不能。quartz拥有timertask所有的功能,而timertask则没有上述,基本说明了在以后的开发中尽可能使用scheduledexecutorservice(jdk1.5以后)替代timer。

五、cron 在线表达式生成器 http://cron.qqe2.com/

附录 cron 表达式

cron表达式用于配置crontrigger的实例。cron表达式实际上是由七个子表达式组成。这些表达式之间用空格分隔。
  • seconds (秒)
  • minutes(分)
  • hours(小时)
  • day-of-month (天)
  • month(月)
  • day-of-week (周)
  • year(年)
例:"0 0 12 ? * wed” 意思是:每个星期三的中午12点执行。个别子表达式可以包含范围或者列表。例如:上面例子中的wed可以换成"mon-fri","mon,wed,fri",甚至"mon-wed,sat"。子表达式范围:
  • seconds (0~59)
  • minutes (0~59)
  • hours (0~23)
  • day-of-month (1~31,但是要注意有些月份没有31天)
  • month (0~11,或者"jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov,dec")
  • day-of-week (1~7,1=sun 或者"sun, mon, tue, wed, thu, fri, sat”)
  • year (1970~2099)
cron表达式的格式:秒 分 时 日 月 周 年(可选)。

字段名 | 允许的值 | 允许的特殊字符 ------- | ------ | ------ | ------ 秒 | 0-59 | , - * / 分 | 0-59 | , - * / 小时 | 0-23 | , - * / 日 | 1-31 | , - * ? / l w c 月 | 1-12 or jan-dec | , - * / 周几 | 1-7 or sun-sat | , - * ? / l c # 年(可选字段) | empty 1970-2099 | , - * /

字符含义:
  • *:代表所有可能的值。因此,“*”在month中表示每个月,在day-of-month中表示每天,在hours表示每小时
  • -:表示指定范围。
  • ,:表示列出枚举值。例如:在minutes子表达式中,“5,20”表示在5分钟和20分钟触发。
  • /:被用于指定增量。例如:在minutes子表达式中,“0/15”表示从0分钟开始,每15分钟执行一次。"3/20"表示从第三分钟开始,每20分钟执行一次。和"3,23,43"(表示第3,23,43分钟触发)的含义一样。
  • ?:用在day-of-month和day-of-week中,指“没有具体的值”。当两个子表达式其中一个被指定了值以后,为了避免冲突,需要将另外一个的值设为“?”。例如:想在每月20日触发调度,不管20号是星期几,只能用如下写法:0 0 0 20 * ?,其中最后以为只能用“?”,而不能用“*”。
  • l:用在day-of-month和day-of-week字串中。它是单词“last”的缩写。它在两个子表达式中的含义是不同的。
  • 在day-of-month中,“l”表示一个月的最后一天,一月31号,3月30号。
  • 在day-of-week中,“l”表示一个星期的最后一天,也就是“7”或者“sat”
  • 如果“l”前有具体内容,它就有其他的含义了。例如:“6l”表示这个月的倒数第六天。“fril”表示这个月的最后一个星期五。
  • 注意:在使用“l”参数时,不要指定列表或者范围,这样会出现问题。
  • w:“weekday”的缩写。只能用在day-of-month字段。用来描叙最接近指定天的工作日(周一到周五)。例如:在day-of-month字段用“15w”指“最接近这个月第15天的工作日”,即如果这个月第15天是周六,那么触发器将会在这个月第14天即周五触发;如果这个月第15天是周日,那么触发器将会在这个月第 16天即周一触发;如果这个月第15天是周二,那么就在触发器这天触发。注意一点:这个用法只会在当前月计算值,不会越过当前月。“w”字符仅能在 day-of-month指明一天,不能是一个范围或列表。也可以用“lw”来指定这个月的最后一个工作日,即最后一个星期五。
  • # :只能用在day-of-week字段。用来指定这个月的第几个周几。例:在day-of-week字段用"6#3" or "fri#3"指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发。

表达式例子:

0 * * * * ? 每1分钟触发一次
0 0 * * * ? 每天每1小时触发一次
0 0 10 * * ? 每天10点触发一次
0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
0 30 9 1 * ? 每月1号上午9点半
0 15 10 15 * ? 每月15日上午10:15触发

*/5 * * * * ? 每隔5秒执行一次
0 */1 * * * ? 每隔1分钟执行一次
0 0 5-15 * * ? 每天5-15点整点触发
0 0/3 * * * ? 每三分钟触发一次
0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点

0 0 12 ? * wed 表示每个星期三中午12点
0 0 17 ? * tues,thur,sat 每周二、四、六下午五点
0 10,44 14 ? 3 wed 每年三月的星期三的下午2:10和2:44触发
0 15 10 ? * mon-fri 周一至周五的上午10:15触发

0 0 23 l * ? 每月最后一天23点执行一次
0 15 10 l * ? 每月最后一日的上午10:15触发
0 15 10 ? * 6l 每月的最后一个星期五上午10:15触发

0 15 10 * * ? 2005 2005年的每天上午10:15触发
0 15 10 ? * 6l 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发

由于篇幅较长,很感谢阅读的朋友能将它读完,希望通过这篇文章能够帮助您对java的定时任务有一个更加深刻的认识和理解!后续会抽时间做个quartz集群,到时分享给大家。感谢~
  • 大小: 15.6 kb
来自:
1
0
评论 共 1 条 请登录后发表评论
1 楼 2017-11-27 09:49
实际应用的时候单点部署的情况已经很少了,可以再出一篇,讨论一下集群场景下的定时任务

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 下面小编就为大家分享一篇java实现web 应用中的定时任务的实例讲解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  • 看完这篇文章你会了解到什么是定时任务,以及为什么austin项目要引入分布式定时任务框架,可以把代码下载下来看到我是怎么使用xxl-job的。 01、如何简单实现定时功能? 我是看视频入门java的,那时候学java基础api的...

  • 2017年11月21日,周二晚上8点30分,7年java开发经验,曾就职一线教育集团java培训高级讲师,现就职硅谷中国系统架构师的alpha带来了主题为《如何用 java 实现web应用中的定时任务?》的交流。以下是主持人maicky整理...

  • java的timer以及timertask类可以...两者结合就可以再web应用中实现定时器功能。话不多说直接上代码1.计划类代码schedulerpublic void sendscheduler(string datestr){final timer timer = new timer();simpledate...

  • 使用javajdk自带的timetask和timer实现定时任务,这种方式可以设置延时,执行间隔,但是不能设置执行时间点,一般用的较少 public class method1 { public static void main(string[] args) { timertask ...

  • 当下,java编码过程中,实现定时任务的方式主要以以下两种为主。网络上关于这两种框架的实践和配置相关的教程很多,这里不再赘述。[email protected]注解的生效原理。1.2.1定时任务执行入口在哪?2.2.2核心方法详解。...

  • 1. 总结常见的实现定时任务的几种方法thread实现 【原理:通过创建一个线程,让他在while循环里面一直运行,用sleep() 方法让其休眠从而达到定时任务的效果。】timer类scheduledexcecutorservice类使用spring的 ...

  • 前言:java开发过程中经常会遇到使用定时任务的情况,比如在某个活动结束时,自动生成获奖名单,导出excel等。常见的有如下四种方式:timer、scheduledexecutorservice、springtask、quartz。 java定时任务的四种...

  • 在java实现简单定时任务一篇记录中描述的是java的几种定时任务的方式,那是最基本的,仅仅是main方法里测试,用途也受限。 这里研究一下在web中怎么实现这些个定时任务,在这儿作个备忘: 在上一篇里我们都是在...

  • 定时任务java实现的几种基本方法。 方法一:thread 是我们最容易想到的,利用while循环,在其中加入sleep方法来实现定时功能。具体代码实现如下 代码示例: public class testtimeorder { public static void ...

  • java schedule现代的 web 应用程序框架在范围和复杂性方面都有所发展...目前 java 系统中实现调度任务的方式大体有一下三种:java 实现调度任务的三种方式一、使用jdk自带的java.util.timer及java.util.timertask类...

  • 前言在实际项目开发中,除了web应用、soa服务外,还有一类不可缺少的,那就是定时任务调度。定时任务的场景可以说非常广泛,比如某些视频网站,购买会员后,每天会给会员送成长值,每月会给会员送一些电影券;比如在...

  • package java.util.concurrent; public interface scheduledexecutorservice extends executorservice { //单次执行,在指定延时delay后运行command任务 public scheduledfuture<?> schedule(runnable ...

  • 在web应用下实现定时任务的简便方法  在web方式下,如果我们要实现定期执行某些任务的话,除了用quartz等第三方开源工具外,我们可以使用timer和timetask来完成指定的定时任务:

  • springboot创建定时任务是比较简单的,我用基于注解@scheduled,这个相对于更简单一些,只是要注意定时时间的设置,我用的版本是不支持时间设置中带lwc这些字符,所以从月末定时更新,我改成了月初更新前一个月的...

global site tag (gtag.js) - google analytics