Java定时器Timer和第三方定时器Quartz

概述

定时器在应用广泛,比如定时统计数据生成报表、每隔设定的时间提醒用户等。Java.util包自带的定时器Timer提供简单的定点执行功能,而Quartz是一个第三方提供的定时器框架。

对比

  • Timer
    • 优点:
      • java.util包自带的,Timer的任务是通过创建TimerTask子类进行实现,使用方便。
    • 缺点:
      • 定时器没有持久性机制。
      • 定时器不能灵活的调度(只能设置开始时间和重复间隔,没有基于日期,一天中的时间等)
      • 定时器不使用线程池(每个定时器一个线程)
      • 定时器没有真正的管理方案,必须编写自己的机制管理。
  • Quartz
    • 优点:
      • Quartz是一个作业调度库,可以与任何其他软件系统集成,也可以和其他软件系统一起使用。
      • Quartz非常灵活,可以灵活、准确的控制日期节点以及执行次数。
      • Quartz非常轻量级,只需要很少的配置即可完成需求,“开箱即用”。
    • 缺点:
      • Quartz必须要新建一个class文件实现Job接口重写execute方法定义任务。

使用方法

Timer

Timer的任务是通过创建TimerTask子类进行实现,定时器由类Timer提供常见功能如下:

  • schedule(TimerTask task, Date time):在time时间点执行task任务一次。
  • schedule(TimerTask task, long delay):在延迟delay毫秒后执行task任务一次。
  • schedule(TimerTask task, Date firstTime, long period):在firsttime时间点执行task一次,之后定期period毫秒时间执行task。时间如果为过去时间, 不会执行过去没有执行的任务, 但是会马上执行。
  • schedule(TimerTask task, long delay, long period):在延迟delay后执行task一次,之后定期period毫秒时间执行task。时间如果为过去时间, 不会执行过去没有执行的任务, 但是会马上执行。

所有delay和period都是long类型的延迟时间,单位为毫秒

指定开始时间

package 定时器;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo {
    public static void main(String[] args) {
        method();
        System.out.println("main执行完成");
    }

    public static void method() {
        // 1、创建Timer对象用于定义定时器的任务及开始时间、周期
        Timer timer = new Timer();

        // 2、创建匿名内部类,定义任务
        TimerTask task = new TimerTask() {
            int count = 1;

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "指定时间定时任务执行中count=" + (count++));
            }
        };

        // 3.任务调度
        timer.schedule(task, new Date());
    }
}

运行结果:
Java定时器Timer和第三方定时器Quartz_第1张图片
结果看出,main执行完成后推出,而定时任务另起线程执行等待。

指定开始时间及执行周期

开始时间为当前时间,每一秒执行一次。

public class TimerDemo {
    public static void main(String[] args) {
        method();
        method2();
        System.out.println("main执行完成");
    }

    public static void method() {
        // 1、创建Timer对象用于定义定时器的任务及开始时间、周期
        Timer timer = new Timer();

        // 2、创建匿名内部类,定义任务
        TimerTask task = new TimerTask() {
            int count = 1;

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "指定时间定时任务执行中count=" + (count++));
            }
        };

        // 3.任务调度
        timer.schedule(task, new Date());
    }

    public static void method2() {
        // 1.创建Timer对象用于定义定时器的任务及开始时间、周期
        Timer timer = new Timer();
        // 创建匿名内部类,定义任务
        TimerTask task = new TimerTask() {
            int count2 = 1;

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "指定时间循环定时任务执行中count2=" + (count2++));
            }
        };
        // 2.任务调度 毫秒值
        timer.schedule(task, new Date(), 1000);
    }
}

运行结果:
Java定时器Timer和第三方定时器Quartz_第2张图片
另起线程执行,每间隔一秒执行一次。

延期执行及执行周期

延迟4秒后执行,每秒执行一次。

public class TimerDemo {
    public static void main(String[] args) {
        method();
        method2();
        method3();
        System.out.println("main执行完成");
    }

    public static void method() {
        ...
    }

    public static void method2() {
        ...
    }

    public static void method3() {
        // 1.创建Timer对象用于定义定时器的任务及开始时间、周期
        Timer timer = new Timer();
        // 创建匿名内部类,定义任务
        TimerTask task = new TimerTask() {
            int count3 = 1;

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "延时循环定时任务执行中count3=" + (count3++));
            }
        };
        // 2.任务调度
        timer.schedule(task, 4000, 1000);
    }
}

运行结果:
Java定时器Timer和第三方定时器Quartz_第3张图片
另起线程执行,延迟4秒后执行,每间隔一秒执行一次。

Quartz

Quartz执行需要1、创建一个SchedulerFactory对象用于生产调度器-Scheduler对象;2、创建调度所需要的任务 任务-Job;3、指定开始的时间和执行周期 触发器-Trigger

需要jar包:quartz-*.jar、slf4j-api-*.jar

示例

package QuartzTest;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import static org.quartz.JobBuilder.newJob;

public class TestQuartz {
    public static void main(String[] args) throws Exception{
        //1、创建工厂对象,用于生产调度器Scheduler对象
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();

        //2、创建任务(JobDetail),具体的任务需要自定义类实现Job接口
        JobDetail jobDetail = newJob(MailJob.class) //指定干活的类MailJob
                .withIdentity("mailjob1", "mailgroup") //定义任务名称和分组
                .usingJobData("email", "[email protected]") //定义属性
                .build();

        //3、定义触发器Trigger,设置开始的时间及周期
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "group1") //定义名称和所属的租
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(1) //每隔2秒执行一次
                        .withRepeatCount(10)) //总共执行11次(第一次执行不基数)
                .startNow()
                .build();

        //4、调度器指定要执行的任务JobDetail及触发器Trigger
        scheduler.scheduleJob(jobDetail, trigger);

        //5、启动
        scheduler.start();

        //6、等待15秒,让前面的任务都执行完了之后,再关闭调度器
        Thread.sleep(15000);
        scheduler.shutdown(true);
        System.out.printf(Thread.currentThread().getName() + " main关闭");
    }
}

自定义任务类MailJob实现Job接口:

package 定时器;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MailJob implements Job {
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDetail detail = context.getJobDetail();
        String email = detail.getJobDataMap().getString("email");

        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        String now = sdf.format(new Date());

        System.out.printf("["+Thread.currentThread().getName()+"]" + new Date() +" 给邮件地址 %s 发出了一封定时邮件, 当前时间是: %s%n" ,email, now);
    }
}

运行结果:
Java定时器Timer和第三方定时器Quartz_第4张图片

.withIdentity(“mailjob1”, “mailgroup”)用于分组。
withIdentity定义任务名称mailjob1和组名mailgroup。比如一个系统有3个job 是备份数据库的,有4个job 是发邮件的,那么对他们进行分组,可以方便管理,类似于一次性停止所有发邮件的这样的操作。

任务-JobDetail

调度所需要的任务-JobDetail。需要新建一个类实现Job接口重写execute方法定义任务。
JobDetail:描述这个Job是做什么的。
JobDataMap: 给Job提供参数用的。通过JobDetail.getJobDataMap获取

  • getString(String key):获取参数值
  • put(String key, String value):设置参数值
public class TestQuartz {
    public static void main(String[] args) throws Exception{
        ...
        
        //2、创建任务(JobDetail),具体的任务需要自定义类实现Job接口
        JobDetail jobDetail = newJob(MailJob.class) //指定干活的类MailJob
                .withIdentity("mailjob1", "mailgroup") //定义任务名称和分组
                .usingJobData("email", "[email protected]") //定义属性
                .build();

        //用JobDataMap 修改email
        jobDetail.getJobDataMap().put("email", "[email protected]");

        ...
    }
}

输出:
Java定时器Timer和第三方定时器Quartz_第5张图片

Job 并发

Quartz定时任务默认都是并发执行的,无论上一次任务是否结束或者完成,只要间隔时间到就会执行下一次, 因为如果定时任执行太长,会长时间占用资源,导致其它任务堵塞。

数据执行任务DatabaseBackupJob:

package QuartzTest;
import org.quartz.*;
import java.util.Date;

public class DatabaseBackupJob implements Job {
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDetail detail = context.getJobDetail();
        String database = detail.getJobDataMap().getString("database");

        System.out.printf("["+Thread.currentThread().getName()+"]" + new Date() +" 给数据库 %s 备份, 耗时10秒 %n" ,database);
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

TestQuartz:

public class TestQuartz {
    public static void main(String[] args) throws Exception{
        databaseCurrentJob();

    }

    private static void databaseCurrentJob() throws Exception {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        Trigger trigger = newTrigger().withIdentity("trigger1", "group1")
                .startNow()
                .withSchedule(simpleSchedule()
                        .withIntervalInSeconds(2)
                        .withRepeatCount(10))
                .build();

        //定义一个JobDetail
        JobDetail jobDetail = newJob(DatabaseBackupJob.class)
                .withIdentity("backupjob", "databasegroup")
                .usingJobData("database", "how2java")
                .build();

        //调度加入这个job
        scheduler.scheduleJob(jobDetail, trigger);

        //启动
        scheduler.start();

       //等待100秒,让前面的任务都执行完了之后,再关闭调度器
        Thread.sleep(100000);
        scheduler.shutdown(true);
    }
}

运行结果:
Java定时器Timer和第三方定时器Quartz_第6张图片
由结果看出,任务并没有等一个任务执行完成,再执行下一个任务。而是等待2秒就执行下一个任务。
但是有时候会做长时间的任务,比如上述数据库备份,这个时候就希望上一次备份成功结束之后,才开始下一次备份,即便是规定时间到了,也不能开始,因为这样很有可能造成数据库被锁死 (几个线程同时备份数据库,引发无法预计的混乱)。

那怎么实现呢?给任务增加注解 @DisallowConcurrentExecution
数据执行任务DatabaseBackupJob:

package QuartzTest;
import org.quartz.*;
import java.util.Date;

@DisallowConcurrentExecution
public class DatabaseBackupJob implements Job {
    public void execute(JobExecutionContext context) throws JobExecutionException {
        ...
    }
}

执行结果:
Java定时器Timer和第三方定时器Quartz_第7张图片
由结果看出,任务会等前一个任务执行完成(执行10秒),才会执行。

Job 异常

任务里发生异常是很常见的。 异常处理办法通常是两种:

  • setUnscheduleAllTriggers:当异常发生,那么就通知所有管理这个 Job 的调度,停止运行它。
  • setRefireImmediately:当异常发生,修改一下参数,马上重新运行。
    ExceptionJob1:
public class ExceptionJob1  implements Job {
    public void execute(JobExecutionContext context) throws JobExecutionException {

        int i = 0;
        try {
            //故意发生异常
            System.out.println(100/i);

        } catch (Exception e) {
            System.out.println("["+Thread.currentThread().getName()+"]" + new Date() + " 发生了异常,取消这个Job 对应的所有调度");
            JobExecutionException je =new JobExecutionException(e);
            je.setUnscheduleAllTriggers(true);
            throw je;
        }
    }
}

ExceptionJob2:

public class ExceptionJob2  implements Job {
    static int i = 0;
    public void execute(JobExecutionContext context) throws JobExecutionException {

        try {
            //故意发生异常
            System.out.println("["+Thread.currentThread().getName()+"]" + new Date() + " 运算结果"+100/i);

        } catch (Exception e) {
            System.out.println("["+Thread.currentThread().getName()+"]" + new Date() + " 发生了异常,修改一下参数,立即重新执行");
            i = 1;
            JobExecutionException je =new JobExecutionException(e);
            je.setRefireImmediately(true);
            throw je;
        }
    }
}

TestQuartz:

public class TestQuartz {
    public static void main(String[] args) throws Exception{
        exceptionHandle1();
        //exceptionHandle2();
    }

    private static void exceptionHandle1() throws Exception {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        Trigger trigger = newTrigger().withIdentity("trigger1", "group1")
                .startNow()
                .withSchedule(simpleSchedule()
                        .withIntervalInSeconds(2)
                        .withRepeatCount(5))
                .build();

        //定义一个JobDetail
        JobDetail jobDetail = newJob(ExceptionJob1.class)
                .withIdentity("exceptionJob1", "someJobGroup")
                .build();

        //调度加入这个job
        scheduler.scheduleJob(jobDetail, trigger);

        //启动
        scheduler.start();

        //等待20秒,让前面的任务都执行完了之后,再关闭调度器
        Thread.sleep(10000);
        scheduler.shutdown(true);
    }

    private static void exceptionHandle2() throws Exception {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        Trigger trigger = newTrigger().withIdentity("trigger1", "group1")
                .startNow()
                .withSchedule(simpleSchedule()
                        .withIntervalInSeconds(2)
                        .withRepeatCount(5))
                .build();

        //定义一个JobDetail
        JobDetail jobDetail = newJob(ExceptionJob2.class)
                .withIdentity("exceptionJob1", "someJobGroup")
                .build();

        //调度加入这个job
        scheduler.scheduleJob(jobDetail, trigger);

        //启动
        scheduler.start();

        //等待20秒,让前面的任务都执行完了之后,再关闭调度器
        Thread.sleep(10000);
        scheduler.shutdown(true);
    }
}

运行结果:

执行exceptionHandle2():

public class TestQuartz {
    public static void main(String[] args) throws Exception{
        //exceptionHandle1();
        exceptionHandle2();
    }
}

运行结果:
Java定时器Timer和第三方定时器Quartz_第8张图片

中断 Job

在业务上,有时候需要中断任务,那么这个Job需要实现 InterruptableJob 接口,才可以被中断。
StoppableJob:

package QuartzTest;
import org.quartz.InterruptableJob;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.UnableToInterruptJobException;

import java.util.Date;

//必须实现InterruptableJob 而非 Job才能够被中断
public class StoppableJob implements InterruptableJob {
    private boolean stop = false;
    public void execute(JobExecutionContext context) throws JobExecutionException {
        while(true){
            if(stop)
                break;
            try {
                System.out.println("["+Thread.currentThread().getName()+"]" + new Date() +" 每隔1秒,进行一次检测,看看是否停止");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("["+Thread.currentThread().getName()+"]" + new Date() +" 持续工作中。。。");
        }

    }
    public void interrupt() throws UnableToInterruptJobException {
        System.out.println("["+Thread.currentThread().getName()+"]" + new Date() +" 被调度叫停");
        stop = true;
    }
}

TestQuartz :

public class TestQuartz {
    public static void main(String[] args) throws Exception{
        stop();
    }

    private static void stop() throws Exception {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        Trigger trigger = newTrigger().withIdentity("trigger1", "group1")
                .startNow()
                .build();

        //定义一个JobDetail
        JobDetail jobDetail = newJob(StoppableJob.class)
                .withIdentity("exceptionJob1", "someJobGroup")
                .build();

        //调度加入这个job
        scheduler.scheduleJob(jobDetail, trigger);

        //启动
        scheduler.start();

        Thread.sleep(5000);
        System.out.println("过5秒,调度停止 job");

        //key 就相当于这个Job的主键
        scheduler.interrupt(jobDetail.getKey());

        //等待20秒,让前面的任务都执行完了之后,再关闭调度器
        Thread.sleep(10000);
        scheduler.shutdown(true);
    }
 }

运行结果:
Java定时器Timer和第三方定时器Quartz_第9张图片

触发器-Trigger

指定开始的时间和执行周期。Trigger 就是触发器的意思,用来指定什么时间开始触发,触发多少次,每隔多久触发一次。
常见触发器 SimpleTrigger、CronTrigger。

SimpleTrigger

常见方法:

  • withIdentity(String name, String group):设置触发器名称、组名
  • startNow():立即执行
  • startAt(Date triggerStartTime):指定时间点执行

10秒后运行

DateBuilder.futureDate():可以方便的获取10秒后, 5分钟后, 3个小时候,2个月后这样的时间。
QuartzDemo:

public class QuartzDemo {
    public static void main(String[] args) throws SchedulerException {
        //1、创建工厂对象,用于生产调度器Scheduler对象
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();

        Date startTime = DateBuilder.futureDate(10, DateBuilder.IntervalUnit.SECOND);

        //2、创建任务(JobDetail),具体的任务需要自定义类实现Job接口
        JobDetail jobDetail = JobBuilder.newJob(MailJob.class) //指定干活的类MailJob
                .withIdentity("mailjob1", "mailgroup") //定义任务名称和分组
                .usingJobData("email", "[email protected]") //定义属性
                .build();

        //3、定义触发器Trigger,设置开始的时间及周期
        SimpleTrigger trigger = (SimpleTrigger) newTrigger().withIdentity("trigger1", "group1")
                .startAt(startTime)
                .build();

        //4、调度器指定要执行的任务JobDetail及触发器Trigger
        Date ft = scheduler.scheduleJob(jobDetail, trigger);

        System.out.println("当前时间是:" + new Date().toLocaleString());
        System.out.printf("%s 这个任务会在 %s 准时开始运行,累计运行%d次,间隔时间是%d毫秒%n", job.getKey(), ft.toLocaleString(), trigger.getRepeatCount()+1, trigger.getRepeatInterval());

        //5、启动
        scheduler.start();
    }
}

运行结果:

累计n次,间隔n秒

  • withSchedule
    • withIntervalInSeconds(n) :每隔n秒执行一次
    • withRepeatCount(n)) :总共执行n+1次(第一次执行不基数)
    • repeatForever():无限重复

QuartzDemo:

...
/3、定义触发器Trigger,设置开始的时间及周期
        SimpleTrigger trigger = (SimpleTrigger) newTrigger().withIdentity("trigger1", "group1")
                .startAt(startTime)
                .withSchedule(simpleSchedule()
                        .withRepeatCount(3)
                        .withIntervalInSeconds(1))
                .build();

运行结果:

CronTrigger

Cron 是Linux下的一个定时器,功能很强大,但是表达式更为复杂。CronTrigger 就是用Cron表达式来安排触发时间和次数的。

Cron表达式见《Cron表达式》

每隔2秒执行一次

...
//3、定义触发器Trigger,设置开始的时间及周期
        CronTrigger trigger = (CronTrigger) newTrigger().withIdentity("trigger1", "group1")
                .withSchedule(cronSchedule("0/2 * * * * ?"))
                .build();

运行结果:

调度器-Scheduler

  • Date scheduleJob(JobDetail var1, Trigger var2):将任务和触发器加入调度器
  • start():启动
  • shutdown():关闭

监听器

Quartz的监听器有Job监听器、Trigger监听器、Scheduler监听器,对不同层面进行监控。实际业务用的较多的是Job监听器,用于监听器是否执行了,其他的用的相对较少,本知识主要讲解Job的。
监听器功能需要创建实现了 JobListener 接口的监听器类。
JobListener接口方法如下:

  • public String getName():返回JobListener名称。对于注册为全局的监听器,getName()主要用于记录日志,对于由特定Job引用的 JobListener,注册在 JobDetail 上的监听器名称必须匹配从监听器上getName()返回值。
  • public void jobToBeExecuted(JobExecutionContext jobExecutionContext):Scheduler在 JobDetail 将要被执行时调用的方法。
  • public void jobExecutionVetoed(JobExecutionContext jobExecutionContext):Scheduler在 JobDetail即将被执行,但又被 Triggeristener否决了调用的方法。
  • public void jobWasExecuted(JobExecutionContext jobExecutionContext, JobExecutionException e):Scheduler在 JobDetail 被执行之后调用的方法。

邮件监听器MailJobListener:

package 定时器;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;

public class MailJobListener implements JobListener {
    @Override
    public String getName() {
        return "listener of mail job";
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext jobExecutionContext) {
        System.out.println("准备执行:\t "+jobExecutionContext.getJobDetail().getKey());
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext jobExecutionContext) {
        System.out.println("取消执行:\t "+jobExecutionContext.getJobDetail().getKey());
    }

    @Override
    public void jobWasExecuted(JobExecutionContext jobExecutionContext, JobExecutionException e) {
        System.out.println("执行结束:\t "+jobExecutionContext.getJobDetail().getKey());
        System.out.println();
    }
}

运行结果:
Java定时器Timer和第三方定时器Quartz_第10张图片

数据库存储

Quartz的触发器、调度、任务等信息都是放在内存中的。不能对执行进度进行实时查看,而且一旦系统异常,信息就会丢失。
所以Quartz还提供了另一个方式,可以把这些信息存放在数据库中,叫做 JobStoreTX。运行状态信息存放在数据库中。

建表

DROP DATABASE IF EXISTS quartz;
CREATE DATABASE quartz DEFAULT CHARACTER SET utf8;
USE quartz;
 
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;
 
CREATE TABLE QRTZ_JOB_DETAILS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    JOB_NAME  VARCHAR(100) NOT NULL,
    JOB_GROUP VARCHAR(100) NOT NULL,
    DESCRIPTION VARCHAR(250) NULL,
    JOB_CLASS_NAME   VARCHAR(250) NOT NULL,
    IS_DURABLE VARCHAR(1) NOT NULL,
    IS_NONCONCURRENT VARCHAR(1) NOT NULL,
    IS_UPDATE_DATA VARCHAR(1) NOT NULL,
    REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
    JOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);
 
CREATE TABLE QRTZ_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(100) NOT NULL,
    TRIGGER_GROUP VARCHAR(100) NOT NULL,
    JOB_NAME  VARCHAR(100) NOT NULL,
    JOB_GROUP VARCHAR(100) NOT NULL,
    DESCRIPTION VARCHAR(250) NULL,
    NEXT_FIRE_TIME BIGINT(13) NULL,
    PREV_FIRE_TIME BIGINT(13) NULL,
    PRIORITY INTEGER NULL,
    TRIGGER_STATE VARCHAR(16) NOT NULL,
    TRIGGER_TYPE VARCHAR(8) NOT NULL,
    START_TIME BIGINT(13) NOT NULL,
    END_TIME BIGINT(13) NULL,
    CALENDAR_NAME VARCHAR(100) NULL,
    MISFIRE_INSTR SMALLINT(2) NULL,
    JOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
        REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)
);
 
CREATE TABLE QRTZ_SIMPLE_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(100) NOT NULL,
    TRIGGER_GROUP VARCHAR(100) NOT NULL,
    REPEAT_COUNT BIGINT(7) NOT NULL,
    REPEAT_INTERVAL BIGINT(12) NOT NULL,
    TIMES_TRIGGERED BIGINT(10) NOT NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
 
CREATE TABLE QRTZ_CRON_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(100) NOT NULL,
    TRIGGER_GROUP VARCHAR(100) NOT NULL,
    CRON_EXPRESSION VARCHAR(100) NOT NULL,
    TIME_ZONE_ID VARCHAR(80),
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
 
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
  (         
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(100) NOT NULL,
    TRIGGER_GROUP VARCHAR(100) NOT NULL,
    STR_PROP_1 VARCHAR(512) NULL,
    STR_PROP_2 VARCHAR(512) NULL,
    STR_PROP_3 VARCHAR(512) NULL,
    INT_PROP_1 INT NULL,
    INT_PROP_2 INT NULL,
    LONG_PROP_1 BIGINT NULL,
    LONG_PROP_2 BIGINT NULL,
    DEC_PROP_1 NUMERIC(13,4) NULL,
    DEC_PROP_2 NUMERIC(13,4) NULL,
    BOOL_PROP_1 VARCHAR(1) NULL,
    BOOL_PROP_2 VARCHAR(1) NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
 
CREATE TABLE QRTZ_BLOB_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(100) NOT NULL,
    TRIGGER_GROUP VARCHAR(100) NOT NULL,
    BLOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
 
CREATE TABLE QRTZ_CALENDARS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    CALENDAR_NAME  VARCHAR(100) NOT NULL,
    CALENDAR BLOB NOT NULL,
    PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
);
 
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_GROUP  VARCHAR(100) NOT NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);
 
CREATE TABLE QRTZ_FIRED_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    ENTRY_ID VARCHAR(95) NOT NULL,
    TRIGGER_NAME VARCHAR(100) NOT NULL,
    TRIGGER_GROUP VARCHAR(100) NOT NULL,
    INSTANCE_NAME VARCHAR(100) NOT NULL,
    FIRED_TIME BIGINT(13) NOT NULL,
    SCHED_TIME BIGINT(13) NOT NULL,
    PRIORITY INTEGER NOT NULL,
    STATE VARCHAR(16) NOT NULL,
    JOB_NAME VARCHAR(100) NULL,
    JOB_GROUP VARCHAR(100) NULL,
    IS_NONCONCURRENT VARCHAR(1) NULL,
    REQUESTS_RECOVERY VARCHAR(1) NULL,
    PRIMARY KEY (SCHED_NAME,ENTRY_ID)
);
 
CREATE TABLE QRTZ_SCHEDULER_STATE
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    INSTANCE_NAME VARCHAR(100) NOT NULL,
    LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
    CHECKIN_INTERVAL BIGINT(13) NOT NULL,
    PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
);
 
CREATE TABLE QRTZ_LOCKS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    LOCK_NAME  VARCHAR(40) NOT NULL,
    PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);
 
commit;

配置文件

Quartz默认加载工程目录下的quartz.properties,如果工程目录下没有,就会去加载quartz.jar包下面的quartz.properties文件。
故,在src下新建 quartz.properties 配置文件,里面指定使用 JobStoreTX 方式管理任务。

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.scheduler.instanceName = MyScheduler
org.quartz.threadPool.threadCount = 3
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource = mysqlDatabase
 
org.quartz.dataSource.mysqlDatabase.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.mysqlDatabase.URL = jdbc:mysql://localhost:3306/quartz?characterEncoding=utf-8
org.quartz.dataSource.mysqlDatabase.user = root
org.quartz.dataSource.mysqlDatabase.password = admin
org.quartz.dataSource.mysqlDatabase.maxConnections = 5

MailJob

和以前的一样,没什么变化。

package 定时器;
import org.quartz.*;
import java.text.SimpleDateFormat;
import java.util.Date;

@DisallowConcurrentExecution
public class MailJob implements Job {
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDetail detail = context.getJobDetail();
        String email = detail.getJobDataMap().getString("email");

        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        String now = sdf.format(new Date());

        System.out.printf("["+Thread.currentThread().getName()+"]" + new Date() +" 给邮件地址 %s 发出了一封定时邮件, 当前时间是: %s(%s) %n" ,email, now,context.isRecovering());
    }
}

TestQuartz

新增加了一个resumeJobFromDatabase 方法,当使用原来的方式增加任务报异常的时候,就直接从数据库重跑任务。

package 定时器;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import static org.quartz.TriggerBuilder.newTrigger;

public class QuartzDemo {
    public static void main(String[] args) throws Exception {
        try {
            assginNewJob();
        } catch (ObjectAlreadyExistsException e) {
            System.err.println("发现任务已经在数据库存在了,直接从数据库里运行:"+ e.getMessage());
            // TODO Auto-generated catch block
            resumeJobFromDatabase();
        }
    }

    private static void resumeJobFromDatabase() throws Exception {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        scheduler.start();
        // 等待200秒,让前面的任务都执行完了之后,再关闭调度器
        Thread.sleep(200000);
        scheduler.shutdown(true);
    }

    private static void assginNewJob() throws SchedulerException, InterruptedException {
        // 创建调度器
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        // 定义一个触发器
        Trigger trigger = newTrigger().withIdentity("trigger1", "group1") // 定义名称和所属的租
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(15) // 每隔15秒执行一次
                        .withRepeatCount(10)) // 总共执行11次(第一次执行不基数)
                .build();

        // 定义一个JobDetail
        JobDetail job = JobBuilder.newJob(MailJob.class) // 指定干活的类MailJob
                .withIdentity("mailjob1", "mailgroup") // 定义任务名称和分组
                .usingJobData("email", "[email protected]") // 定义属性
                .build();

        // 调度加入这个job
        scheduler.scheduleJob(job, trigger);

        // 启动
        scheduler.start();

        // 等待20秒,让前面的任务都执行完了之后,再关闭调度器
        Thread.sleep(20000);
        scheduler.shutdown(true);
    }
}

第一次运行:

QRTZ_SIMPLE_TRIGGERS:
Java定时器Timer和第三方定时器Quartz_第11张图片
QRTZ_TRIGGERS:
Java定时器Timer和第三方定时器Quartz_第12张图片
QRTZ_JOB_DETAILS:
Java定时器Timer和第三方定时器Quartz_第13张图片

第二次运行:
Java定时器Timer和第三方定时器Quartz_第14张图片
注意:如果任务执行完成,上述数据库表数据均被清空。如果在执行期间,查看数据库表,表QRTZ_TRIGGERS、QRTZ_JOB_DETAILS数据不变,表QRTZ_SIMPLE_TRIGGERS会实时记录执行次数:
Java定时器Timer和第三方定时器Quartz_第15张图片
字段REPEAT_COUNT记录还需要执行的总次数,字段TIMES_TRIGGERED记录执行过的次数。

Quartz集群

所谓的Quartz集群,是指在基于数据库存储Quartz调度信息的基础上,有多个一模一样的 Quartz 应用在运行。
当某一个Quartz应用重启或者发生问题的时候,其他的Quartz应用会借助数据库这个桥梁探知到它不行了,从而接手把该进行的Job调度工作进行下去。
以这种方式保证任务调度的高可用性,即在发生异常重启等情况下,调度信息依然连贯性地进行下去,就好像Quartz应用从来没有中断过似的。
Java定时器Timer和第三方定时器Quartz_第16张图片

quartz.properties

quartz.properties 在原来的基础上,增加3行:

org.quartz.jobStore.isClustered = true
org.quartz.scheduler.instanceId = AUTO
org.quartz.jobStore.clusterCheckinInterval = 1000

...
  • org.quartz.jobStore.isClustered = true:开启集群
  • org.quartz.scheduler.instanceId = AUTO:要进行集群,多个应用调度id instanceId 必须不一样,这里使用AUTO,就会自动分配不同的ID。 目测是本机机器名称加上时间戳
  • org.quartz.jobStore.clusterCheckinInterval = 1000:每个一秒钟去数据库检查一下,在其他应用挂掉之后及时补上

注:要进行集群,多个应用调度名称 instanceName 应该是一样的。

TestQuartz

TestQuartz 不需要做改动,本例增加一些输出信息。
启动步骤:

  1. 启动一次 TestQuartz,叫做 a 应用
  2. 紧接着(在几秒钟内)再次启动 TestQuartz,叫做 b 应用
  3. 使用多控制台显示方式,在两个不同的控制台观察现象

上述相当于两个应用做了集群。
运行结果:
应用a:
Java定时器Timer和第三方定时器Quartz_第17张图片
应用b:
Java定时器Timer和第三方定时器Quartz_第18张图片
应用a先执行,运行20秒,就自动结束了。
应用b在应用a执行期间,不会执行。
应用b在应用b执行结束之后,检测到任务还未完成,自动把后续任务执行完毕。

你可能感兴趣的:(Java,java,开发语言,后端)