SpringBoot和SSM的Quartz定时器

x33g5p2x  于2021-09-22 转载在 Spring  
字(49.5k)|赞(0)|评价(0)|浏览(954)

SpringBoot-Quartz定时器

Quartz定时器介绍

Quartz是什么

简单来说就是一个任务调度框架,帮我们自动去执行这些程序。

大部分公司都会用到定时任务这个功能。

比如订单支付来说,超过一定时间会执行这个job,去判断你是否支付,未支付就会取消此次订单

Quartz能干什么

1: 下载交易流水

比如银行每天会自动下载流水

2: 修改密码提醒和告警

出于安全考虑,长时间没有修改密码会发出警告

3: 会员续费

会员到期了,TX提醒你进行充值

4: 短信通知

每隔一段时间向指定用户发送短信

5:垃圾清理

比如:图片上传时候用户没有提提交表单 那么这张图片就是垃圾图片我们需要进行定时清理这些垃圾图片

…太多太多能用到定时器的地方了这里就不在举例了

需要的Maven

<dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz</artifactId>
        <version>2.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz-jobs</artifactId>
        <version>2.2.1</version>
    </dependency>

Quartz的原理

Quartz的原理不是很复杂,只要搞明白几个概念,然后知道如何去启动和关闭一个调度程序即可。(关闭一般用不到)

1、Job
表示一个工作,要执行的具体内容。此接口中只有一个方法
void execute(JobExecutionContext context)

2、JobDetail
JobDetail表示一个具体的可执行的调度程序,Job是这个可执行程调度程序所要执行的内容,另外JobDetail还包含了这个任务调度的方案和策略。

3、Trigger代表一个调度参数的配置,什么时候去调。

4、Scheduler代表一个调度容器,一个调度容器中可以注册多个JobDetail和Trigger。当Trigger与JobDetail组合,就可以被Scheduler容器调度了。

Quartz入门案例

然后我们看代码 注释写的很清楚

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class TestQuartz {


    public static void main(String[] args) throws Exception{
        //创建调度器
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        //定义一个触发器
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "group1") //定义触发器名称和所属触发器的分组
                .startNow()
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever()
                        .withIntervalInSeconds(2) //每隔2秒执行一次
                        .withRepeatCount(10)) //总共执行11次(第一次执行不记数)
                .build();

        //定义一个JobDetail (任务)
        JobDetail job = JobBuilder.newJob(MailJob.class) //指定干活的类MailJob
                .withIdentity("mailjob1", "mailgroup") //定义任务名称和所属任务的分组
                .usingJobData("email", "admin@10086.com") //定义传入任务里的属性(key,value)
                .build();

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

        //启动任务
        scheduler.start();

        //等待20秒,让前面的任务都执行完了之后,再关闭调度器
        Thread.sleep(20000);
        scheduler.shutdown(true);
    }
}
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 {
    //执行任务的唯一入口
    @Override
    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("给邮件地址 %s 发出了一封定时邮件, 当前时间是: %s%n" ,email, now);
    }

}

结果:

给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 09:39:18
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 09:39:20
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 09:39:22
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 09:39:24
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 09:39:26
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 09:39:28
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 09:39:30
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 09:39:32
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 09:39:34
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 09:39:36
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 09:39:38

分组干什么用?

.withIdentity(“mailjob1”, “mailgroup”)

mailgroup就是分组的意思。
比如一个系统有3个job 是备份数据库的,有4个job 是发邮件的,那么对他们进行分组,可以方便管理,类似于一次性停止所有发邮件的这样的操作.

静态导入

import static 这种写法叫做静态导入,指的是导入某个类的静态方法, 这样就可以直接使用了,

newTrigger() newJob

而不是必须写成:TriggerBuilder.newTrigger() JobBuilder.newJob()

import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;

自己将上面的代码进行修改下

Job管理

Job 组成部分

Job 其实是由 3 个部分组成:

JobDetail: 用于描述这个Job是做什么的
1.
实现Job的类: 具体干活的
1.
JobDataMap: 给 Job 提供参数用的

JobDataMap 除了usingJobData 方式之外,还可以是其他方式,像这样
job.getJobDataMap().put(“email”, “admin@taobao.com”);

关键代码

//定义一个JobDetail
        JobDetail job = newJob(MailJob.class)
            .withIdentity("mailjob1", "mailgroup")
            .usingJobData("email", "admin@10086.com")
            .build();
         
        //用JobDataMap 修改email
        job.getJobDataMap().put("email", "admin@taobao.com");
Job 并发

默认的情况下,无论上一次任务是否结束或者完成,只要规定的时间到了,那么下一次就开始。

有时候会做长时间的任务,比如数据库备份,这个时候就希望上一次备份成功结束之后,才开始下一次备份,即便是规定时间到了,也不能开始,因为这样很有可能造成 数据库被锁死 (几个线程同时备份数据库,引发无法预计的混乱)。

那么在这种情况下,给数据库备份任务类增加一个注解就好了:
@DisallowConcurrentExecution

Job 异常

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

  1. 当异常发生,那么就通知所有管理这个 Job 的调度,停止运行它
  2. 当异常发生,修改异常地方,然后重新运行

Job任务

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("发生了异常,取消这个Job 对应的所有调度");
            JobExecutionException je =new JobExecutionException(e);
            je.setUnscheduleAllTriggers(true);
            throw je;
        }
    }
}

测试

public static void main(String[] args) throws Exception{
        exceptionHandle1();

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

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

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

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

        //启动
        scheduler.start();

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

在业务上,有时候需要中断任务,那么这个Job需要实现 InterruptableJob 接口,然后就方便中断了

Job任务

//必须实现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("每隔1秒,进行一次检测,看看是否停止");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("持续工作中。。。");
        }

    }
    public void interrupt() throws UnableToInterruptJobException {
        System.out.println("被调度叫停");
        stop = true;
    }
}

测试

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 job = newJob(StoppableJob.class)
                .withIdentity("exceptionJob1", "someJobGroup")
                .build();

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

        //启动
        scheduler.start();

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

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

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

}

SimpleTrigger

Trigger 就是触发器的意思,用来指定什么时间开始触发,触发多少次,每隔多久触发一次.
SimpleTrigger 可以方便的实现一系列的触发机制。

下一个8秒的倍数

job任务

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

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

        System.out.printf("发出了一封邮件, 当前时间是: %s%n" , now);
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试

public static void main(String[] args) throws Exception{
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        Date startTime = DateBuilder.nextGivenSecondDate(null, 8);

        JobDetail job = newJob(MailJob.class).withIdentity("mailJob", "mailGroup").build();

        SimpleTrigger trigger = (SimpleTrigger) newTrigger().withIdentity("trigger1", "group1").startAt(startTime).build();

        // schedule it to run!
        Date ft = scheduler.scheduleJob(job, trigger);

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

        scheduler.start();

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

当前时间是:2021-3-7 22:41:27
mailGroup.mailJob 这个任务会在 2021-3-7 22:41:32 准时开始运行,累计运行1次,间隔时间是0毫秒
发出了一封邮件, 当前时间是: 22:41:32

10 秒后运行

使用方法:

DateBuilder.futureDate()

可以方便的获取10秒后, 5分钟后, 3个小时候,2个月后…这样的时间

job任务 还是上面那个案例MailJob的类

测试

public static void main(String[] args) throws Exception{
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

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

        JobDetail job = newJob(MailJob.class).withIdentity("mailJob", "mailGroup").build();

        SimpleTrigger trigger = (SimpleTrigger) newTrigger().withIdentity("trigger1", "group1").startAt(startTime).build();

        // schedule it to run!
        Date ft = scheduler.scheduleJob(job, trigger);

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

        scheduler.start();

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

当前时间是:2021-3-7 22:46:57
mailGroup.mailJob 这个任务会在 2021-3-7 22:47:07 准时开始运行,累计运行1次,间隔时间是0毫秒
发出了一封邮件, 当前时间是: 22:47:07

注意 DateBuilder.IntervalUnit这里面包含了

MILLISECOND 毫秒

SECOND 秒

MINUTE分

HOUR时

DAY天

WEEK周

MONTH 月

YEAR年

累计n次,间隔n秒

job任务 还是上面那个案例MailJob的类

测试

public static void main(String[] args) throws Exception{
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        Date startTime = DateBuilder.nextGivenSecondDate(null, 8);

        JobDetail job = newJob(MailJob.class).withIdentity("mailJob", "mailGroup").build();

        SimpleTrigger trigger = (SimpleTrigger) newTrigger()
                .withIdentity("trigger1", "group1")
                .startAt(startTime)
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever()
                        .withRepeatCount(3)//执行次数 算上原本的一次一共4次
                        .withIntervalInSeconds(1)) //每一秒执行一次
                .build();

        // schedule it to run!
        Date ft = scheduler.scheduleJob(job, trigger);

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

        scheduler.start();

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

当前时间是:2021-3-7 22:54:45
mailGroup.mailJob 这个任务会在 2021-3-7 22:54:48 准时开始运行,累计运行4次,间隔时间是1000毫秒
发出了一封邮件, 当前时间是: 22:54:48
发出了一封邮件, 当前时间是: 22:54:49
发出了一封邮件, 当前时间是: 22:54:50
发出了一封邮件, 当前时间是: 22:54:51

无限重复,间隔1秒

job任务 还是上面那个案例MailJob的类

测试

public static void main(String[] args) throws Exception{
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        Date startTime = DateBuilder.nextGivenSecondDate(null, 8);

        JobDetail job = newJob(MailJob.class).withIdentity("mailJob", "mailGroup").build();

        SimpleTrigger trigger = (SimpleTrigger) newTrigger()
                .withIdentity("trigger1", "group1")
                .startAt(startTime)
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever()
                        .repeatForever() //表示无限次数
                        .withIntervalInSeconds(1)) //每1秒执行一次
                .build();

        // schedule it to run!
        Date ft = scheduler.scheduleJob(job, trigger);

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

        scheduler.start();

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

无限重复的累计次数是显示的0,因为没法表示无限。。

CronTrigger

CronTrigger 是什么?

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

因为Cron 特别复杂,如果以前接触过,可以按照Cron表达式来做触发器,没有接触过,还是用SimpleTrigger吧。。。

这个Cron 表达式就表示每隔2秒执行一次
0/2 /* /* /* /* ?

job任务 还是上面那个案例MailJob的类

public static void main(String[] args) throws Exception{
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        Date startTime = DateBuilder.nextGivenSecondDate(null, 8);

        JobDetail job = newJob(MailJob.class).withIdentity("mailJob", "mailGroup").build();

        CronTrigger trigger = newTrigger().withIdentity("trigger1", "group1").withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?"))
                .build();

        // schedule it to run!
        Date ft = scheduler.scheduleJob(job, trigger);

        System.out.println("使用的Cron表达式是:"+trigger.getCronExpression());
// System.out.printf("%s 这个任务会在 %s 准时开始运行,累计运行%d次,间隔时间是%d毫秒%n", job.getKey(), ft.toLocaleString(), trigger.getRepeatCount()+1, trigger.getRepeatInterval());

        scheduler.start();

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

发出了一封邮件, 当前时间是: 23:03:28
发出了一封邮件, 当前时间是: 23:03:30
发出了一封邮件, 当前时间是: 23:03:32
发出了一封邮件, 当前时间是: 23:03:34
发出了一封邮件, 当前时间是: 23:03:36
发出了一封邮件, 当前时间是: 23:03:38
发出了一封邮件, 当前时间是: 23:03:40
发出了一封邮件, 当前时间是: 23:03:42
发出了一封邮件, 当前时间是: 23:03:44
发出了一封邮件, 当前时间是: 23:03:46

如结果所示 每隔2秒执行一次。为什么这个表达式就是这个效果呢? 看下一步 理解Cron

理解Cron

0/2 /* /* /* /* ?

由7个部分组成,每个部分就如图所示分别对应 秒分 一直到年

星号(/*):可用在所有字段中,表示对应时间域的每一个时刻,例如, 在分钟字段时,表示“每分钟”;
问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符;
减号(-):表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12;
逗号(,):表达一个列表值,如在星期字段中使用“MON,WED,FRI”,则表示星期一,星期三和星期五;

这里有个Cron表达式生成工具,可以参考一下: http://cron.qqe2.com/

但是注意这个网站生成的是标准的7位 但是在java中只能是6位 需要把最后的/*去掉

比如: 0 59 23 /* /* ?/* 在java中是 0 59 23 /* /* ?

Cron 表达式举例

监听器

Quartz 的监听器有Job监听器,Trigger监听器, Scheduler监听器,对不同层面进行监控。 实际业务用的较多的是Job监听器,用于监听器是否执行了,其他的用的相对较少,本知识主要讲解Job的。

MailJobListener 实现了 JobListener 接口,就必须实现如图所示的4个方法。

job任务 还是上面那个案例MailJob的类

创建监听类 MailJobListener

public class MailJobListener implements JobListener {

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

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

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

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

}

测试

public static void main(String[] args) throws Exception{
        //创建调度器
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        //定义一个触发器
        Trigger trigger = newTrigger().withIdentity("trigger1", "group1") //定义名称和所属的租
                .startNow()
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever()
                        .withIntervalInSeconds(2) //每隔2秒执行一次
                        .withRepeatCount(10)) //总共执行11次(第一次执行不记数)
                .build();

        //定义一个JobDetail
        JobDetail mailJob = newJob(MailJob.class) //指定干活的类MailJob
                .withIdentity("mailjob1", "mailgroup") //定义任务名称和分组
                .usingJobData("email", "admin@10086.com") //定义属性
                .build();

        //增加Job监听
        MailJobListener mailJobListener= new MailJobListener();
        KeyMatcher<JobKey> keyMatcher = KeyMatcher.keyEquals(mailJob.getKey());
        scheduler.getListenerManager().addJobListener(mailJobListener, keyMatcher);

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

        //启动
        scheduler.start();

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

JDBCStore

JDBCStore 概念

默认情况,Quartz的触发器,调度,任务等信息都是放在内存中的,叫做 RAMJobStore。 好处是快速,坏处是一旦系统重启,那么信息就丢失了,就得全部从头来过。
所以Quartz还提供了另一个方式,可以把这些信息存放在数据库做,叫做 JobStoreTX。 好处是就算系统重启了,目前运行到第几次了这些信息都是存放在数据库中的,那么就可以继续原来的步伐把计划任务无缝地继续做下去。 坏处就是性能上比内存慢一些,毕竟数据库读取总是要慢一些的。

建表 sql

为了能够把相关信息存放进 mysql 数据库里,必须手动建立数据库和表,使用如下 脚本就行了。
注: 这里使用的数据库名称是 quartz

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;

配置文件

在src下的resources内新建 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.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
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 = root
org.quartz.dataSource.mysqlDatabase.maxConnections = 5

注意添加JDBC驱动包

<!--java JDBC-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>

job任务

@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("给邮件地址 %s 发出了一封定时邮件, 当前时间是: %s (%s)%n" ,email, now,context.isRecovering());
    }
}

测试

public class TestQuartz {
    public static void main(String[] args) throws Exception {
        try {
            assginNewJob();
        } catch (ObjectAlreadyExistsException e) {
            System.err.println("发现任务已经在数据库存在了,直接从数据库里运行:"+ e.getMessage());
            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.repeatSecondlyForever().withIntervalInSeconds(15) // 每隔15秒执行一次
                        .withRepeatCount(10)) // 总共执行11次(第一次执行不基数)
                .build();

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

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

        // 启动
        scheduler.start();

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

第一次运行的结果

给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 00:56:42 (false)
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 00:56:56 (false)

第二次运行的结果

发现任务已经在数据库存在了,直接从数据库里运行:Unable to store Job : ‘mailgroup.mailjob1’, because one already exists with this identification.

给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 00:57:12 (false)
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 00:57:26 (false)
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 00:57:41 (false)
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 00:57:56 (false)
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 00:58:11 (false)
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 00:58:26 (false)
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 00:58:41 (false)
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 00:58:56 (false)
给邮件地址 admin@10086.com 发出了一封定时邮件, 当前时间是: 00:59:11 (false)

解析这是为什么

jDBCStore每次执行任务前都会去数据库里找这个任务有没有,没有执行完的任务如果有就接着上回继续执行

如果没有就 按照现在的新任务开始

上面代码结果解析:

你第一次运行时候会去数据库里找发现没有此任务,未执行完的数据,那么就按照现在的新任务开始然后你发现

发现你执行2次就关闭了,这是我故意的

注意:看代码我们只让主线程停止了20每秒而job任务是每15秒执行一次也就是开始的时候执行一次 然后过15秒后执行一次之后就运行了scheduler.shutdown(true);将任务强制关闭了.

主要是测试是否下次从新运行此任务是否从上次断开的时候继续运行剩下的次数

然后你去数据库qrtz_simple_triggers表里去看看 这个表里记录里一共需要执行几次和你当前执行了几次

然后你第二次执行的时候他会从数据库里看看有没有,没有运行完毕的任务记录如果有就接着上回继续执行

直到执行完剩下的9次后自动将数据库数据删除,你在去qrtz_simple_triggers表里就会发现没数据了

集群概念

这所谓的Quartz集群,是指在 基于数据库存储 Quartz调度信息的基础上, 有多个一模一样的 Quartz 应用在运行。
当某一个Quartz 应用重启或者发生问题的时候, 其他的Quartz 应用会借助数据库这个桥梁探知到它不行了,从而接手把该进行的Job调度工作进行下去。
以这种方式保证任务调度的高可用性,即在发生异常重启等情况下,调度信息依然连贯性地进行下去,就好像 Quartz 应用从来没有中断过似的。
注: 文中描述的 Quartz 应用 在一些语境下,又叫做 Quartz 服务器节点,都是同一个概念。

TestQuartz 不需要做改动,沿用 JDBC Store里的 TestQuartz.java 就行

而表也沿用 JDBC Store里的不要动还是原来的

quartz.properties 在原来的基础上,增加和修改下这4行:

开启集群
org.quartz.jobStore.isClustered = true

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

org.quartz.scheduler.instanceName = quartzScheduler

要进行集群,多个应用调度id instanceId 必须不一样,这里使用AUTO,就会自动分配不同的ID。 目测是本机机器名称加上时间戳

org.quartz.scheduler.instanceId = AUTO

每个一秒钟去数据库检查一下,以在其他应用挂掉之后及时补上

org.quartz.jobStore.clusterCheckinInterval = 1000

完整版

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

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 = root
org.quartz.dataSource.mysqlDatabase.maxConnections = 5

job任务还是 JDBCStore里的MailJob类

测试代码

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

    private static void resumeJobFromDatabase() throws Exception {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        System.out.println("当前调度器的id是:"+scheduler.getSchedulerInstanceId());
        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.repeatSecondlyForever()
                        .withIntervalInSeconds(15) // 每隔15秒执行一次
                        .withRepeatCount(10)) // 总共执行11次(第一次执行不基数)
                .build();

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

        // 调度加入这个job
        scheduler.scheduleJob(job, trigger);
        System.out.println("当前调度器的id是:"+scheduler.getSchedulerInstanceId());

        // 启动
        scheduler.start();

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

Quartz定时工具类

实体类BaseJob

public class BaseJob implements Serializable {

    private String jobName;   //任务名称
    private String jobGroupName;  //任务组名称
    private String triggerName; //触发器名称
    private String triggerGroupName;//触发器组名称
    private String cronTime;  //crond格式时间
    private JobDataMap jobDataMap;  //附带参数
    private  int time;  //时间间隔 秒
    private  int  count; //执行多少次
    private Class jobClass;
 
    ......get  set
}

QuartzUtil

public class QuartzUtil {
    /** * [简单任务调度:每次执行间隔为多少毫秒,执行多少次] <br> */
    public static void handleSimpleTrigger(BaseJob baseJob) {

        // 通过schedulerFactory获取一个调度器
        SchedulerFactory schedulerfactory = new StdSchedulerFactory();
        Scheduler scheduler = null;
        try {
            // 通过schedulerFactory获取一个调度器
            scheduler = schedulerfactory.getScheduler();
            // 创建jobDetail实例,绑定Job实现类
            // 指明job的名称,所在组的名称,以及绑定job类
            JobDetail job = JobBuilder.newJob(baseJob.getJobClass())
                    .withIdentity(baseJob.getJobName(),baseJob.getJobGroupName() )
                    .usingJobData(baseJob.getJobDataMap())
                    .build();
            // 定义调度触发规则
            //使用simpleTrigger规则
            Trigger
                    trigger=TriggerBuilder.newTrigger().withIdentity(baseJob.getTriggerName(),
                    baseJob.getJobGroupName())
                    .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(baseJob.getTime()).withRepeatCount(baseJob.getCount()))
                    .startNow().build();
            // 把作业和触发器注册到任务调度中
            scheduler.scheduleJob(job, trigger);
            // 启动调度
            scheduler.start();

        } catch (Exception e) {
                e.printStackTrace();
        }


    }
    /** * [复杂任务调度:每天几点几分几秒定时执行任务] */
    public static void hadleCronTrigger(BaseJob baseJob) {
        // 通过schedulerFactory获取一个调度器
        SchedulerFactory schedulerfactory = new StdSchedulerFactory();
        Scheduler scheduler = null;
        try {
            // 通过schedulerFactory获取一个调度器
            scheduler = schedulerfactory.getScheduler();
            // 创建jobDetail实例,绑定Job实现类
            // 指明job的名称,所在组的名称,以及绑定job类
            JobDetail job = JobBuilder.newJob(baseJob.getJobClass())
                    .withIdentity(baseJob.getJobName(), baseJob.getJobGroupName())
                    .usingJobData(baseJob.getJobDataMap())
                    .build();
            // 定义调度触发规则
            //使用cornTrigger规则 每天18点30分
            Trigger trigger=TriggerBuilder.newTrigger().withIdentity(baseJob.getTriggerName(), baseJob.getTriggerGroupName())
                    .withSchedule(CronScheduleBuilder.cronSchedule(baseJob.getCronTime()))
                    .startNow().build();
            // 把作业和触发器注册到任务调度中
            scheduler.scheduleJob(job, trigger);
            // 启动调度
            scheduler.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }



}

这个工具类主要就是 各种调用job任务的

然后我们开始测试

创建2个job任务

MyJob 和 My1Job

public class MyJob implements Job {

    @Override
    public void execute(JobExecutionContext context)  {
        System.out.println("MyJob Hello World");
        // 编写具体的业务逻辑
        JobKey key = context.getJobDetail().getKey();
        System.out.println("当前任务名称:" + key.getName() + " 当前任务组名称:: " + key.getGroup());

        TriggerKey trkey = context.getTrigger().getKey();
        System.out.println("当前触发器的名称: " + trkey.getName() + " 当前触发器组的名称: " + trkey.getGroup());

        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        Object test = jobDataMap.get("test");
        System.out.println(test);
    }

}
public class My1Job implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("My1Job----- Hello World");
// 编写具体的业务逻辑
        JobKey key = context.getJobDetail().getKey();
        System.out.println("当前任务名称:" + key.getName() + " 当前任务组名称:: " + key.getGroup());

        TriggerKey trkey = context.getTrigger().getKey();
        System.out.println("当前触发器的名称: " + trkey.getName() + " 当前触发器组的名称: " + trkey.getGroup());

        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        Object test = jobDataMap.get("test1");
        System.out.println(test);
        
    }

}

测试类Test

public class Test {

    public static void main(String[] args) {


        BaseJob baseJob = new BaseJob();
        baseJob.setJobClass(MyJob.class);
        baseJob.setJobName("jobname");//任务名称
        baseJob.setJobGroupName("jobgroupname");//任务组名称
        baseJob.setTriggerName("triggerName");//触发器名称
        baseJob.setTriggerGroupName("triggerGroupNam");//触发器组名称
        JobDataMap jobDataMap = new JobDataMap(); //向job传入参数
        jobDataMap.put("test", "test");
        baseJob.setJobDataMap(jobDataMap);
        baseJob.setTime(1);//每秒执行1次
        baseJob.setCount(8);//执行8次结束

        //简单任务调度,每隔多少时间执行一次,执行n次
        QuartzUtil.handleSimpleTrigger(baseJob);

        BaseJob baseJob1 = new BaseJob();
        baseJob1.setJobClass(My1Job.class);
        baseJob1.setJobName("jobname1");//任务名称
        baseJob1.setJobGroupName("jobgroupname1");//任务组名称
        baseJob1.setTriggerName("triggerName1");//触发器名称
        baseJob1.setTriggerGroupName("triggerGroupNam1");//触发器组名称
        JobDataMap jobDataMap1 = new JobDataMap(); //向job传入参数
        jobDataMap1.put("test1", "test1");
        baseJob1.setJobDataMap(jobDataMap1);
        baseJob1.setCronTime("*/3 * * * * ?");  //cron语法 每3秒执行一次 一直执行

        //复杂调度,每天的什么时候执行任务
        QuartzUtil.hadleCronTrigger(baseJob1);
    }

}

我们一般都是配合spring进行使用的请接着往下看

Spring-Quartz定时器用法

我们就不搞乱七八糟的了 直接使用最佳方案 Quartz注解版 而一般情况使用Quartz都会搭配着数据库进行使用…

小二上代码 来了客官…

项目结构

需要的Maven

<dependencies>
       <!--quartz-->
    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz</artifactId>
        <version>2.2.3</version>
    </dependency>
    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz-jobs</artifactId>
        <version>2.2.3</version>
    </dependency>
    
       <!--spring-->
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.5.RELEASE</version>
    </dependency>
        <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.5</version>
    </dependency>
    
    <!--java JDBC-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.4.6</version>
    </dependency>

    <!--java druid 连接-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.12</version>
    </dependency>

    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    
</dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.*</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
</project>

配置文件和表

druid.properties

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&failOverReadOnly=false
name=root
pass=root
# 初始化连接数量
initialSize=5
#最小空闲连接
minIdle=3
# 最大连接数
maxActive=10
# 最大超时时间  3秒
maxWait=3000

log4j.properties

## 设置Logger输出级别和输出目的地(可以指定多个目的地) ###
### 一般在开发的时候使用debug,开发完成后使用error ###
### 他们对应的是输出信息的级别,级别越低信息输出越详细,使用debug级别的时候,info中的信息也能输出,使用info的时候,debug对应的信息显示不出来 ###
### 日志记录器输出级别:fatal>error>warn>info>debug ###
### 后面的两个对应下方的两处 一处打印在控制台 另一处打印在日志文件里
log4j.rootLogger=debug,console,logFile

#############################################################################################
### 把日志信息输出到控制台 ###
log4j.appender.console=org.apache.log4j.ConsoleAppender
### 信息打印到System.err上,红色 ###
log4j.appender.console.Target=System.out
### 指定日志在控制台上输出的布局类型  采用指定格式的类型 ###
log4j.appender.console.layout=org.apache.log4j.PatternLayout
### %r:输出自应用启动到输出该 log信息耗费的毫秒数    %x表示信息输出时左对齐
### %5p:%p表示输出日志信息优先级,即 DEBUG, INFO, WARN, ERROR, FATAL 中间的5控制最小的宽度为5
### %F:%L %F:输出日志消息产生时所在的文件名称   %L:输出代码中的行号
### %l:输出日志事件的发生位置,相当于%C.%M(%F:%L)的组合,包括类目名、发生的线程,以及行数
### %m:输出代码中指定的消息,产生的日志具体信息  %n:输出一个回车换行符, Windows 平台为"\r\n", Unix 平台为"\n"输出日志信息换行
log4j.appender.console.layout.ConversionPattern= -> (%r ms) - %d{yyyy-M-d HH:mm:ss}%x[%5p](%F:%L) %m%n
#############################################################################################

#############################################################################################
### 把日志信息输出到文件:logt.log 注意:如果有路径\符号一定要写成\\ 否则会被转义  ###
log4j.appender.logFile=org.apache.log4j.FileAppender
### 指定日志输出的文件名 ###
log4j.appender.logFile.File=D:\\log.log
### 指定转换模式 ###
log4j.appender.logFile.layout=org.apache.log4j.PatternLayout
### 指定日志布局类型 ###
###log4j.appender.logFile.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss}%l %F %p %m%n
###log4j.appender.logFile.layout.ConversionPattern= -> (%r ms) - %d{yyyy-M-d HH:mm:ss}%x[%5p](%F:%L) %m%n
log4j.appender.logFile.layout.ConversionPattern= -> (%r ms) - %d{yyyy-MM-dd HH:mm:ss}%x[%5p]%l %m%n
#############################################################################################

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 配置日志-->
    <settings>
        <setting name="logImpl" value="LOG4J"/>
        <!-- 懒加载-->
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"/>
        <!-- 开启二级缓存-->
        <setting name="cacheEnabled" value= "true" />
    </settings>
    <!-- 别名-->
    <typeAliases>
        <package name="cn.quartz.pojo"/>
    </typeAliases>

    <mappers>
        <package name="com.htsa.dao"/>
    </mappers>

</configuration>

spring核心配置文件 ApplicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation=" http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd ">


    <!--开启Quartz注解驱动-->
    <task:annotation-driven/>
    <!--包扫描-->
    <context:component-scan base-package="cn.quartz"/>


    <!-- 配置数据源 使用外部druid.properties配置文件-->
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:druid.properties"/>
    </bean>

    <!-- 连接数据库-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 基本属性 url、user、password -->
        <property name="driverClassName" value="${driverClassName}"/>
        <property name="url" value="${url}" />
        <property name="username" value="${name}" />
        <property name="password" value="${pass}" />
        <!-- 初始化连接数量-->
        <property name="initialSize" value="${initialSize}" />
        <!-- 最小连接数-->
        <property name="minIdle" value="${minIdle}" />
        <!-- 最大连接数 -->
        <property name="maxActive" value="${maxActive}" />
        <!-- 最大超时时间 3秒-->
        <property name="maxWait" value="${maxWait}"/>
    </bean>

    <!-- 配置sqlSessionFactory工厂 有了这个就可以使用Mybatis接口了-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"> </property>
    </bean>
    <!--获取 sqlSession-->
    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"/>
    </bean>

    <!-- 配置Dao层下接口 自动生成Mapper实现类 类名为 接口首字母小写 -->
    <!-- MapperScannerConfigurer内部会自动调用SqlSessionFactoryBean-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
        <property name="basePackage" value="cn.quartz.mapper"/>
    </bean>

    <!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- 配置事务通知属性 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!-- 定义事务传播属性 -->
        <tx:attributes>
            <tx:method name="insert*" propagation="REQUIRED" />
            <tx:method name="update*" propagation="REQUIRED" />
            <tx:method name="edit*" propagation="REQUIRED" />
            <tx:method name="save*" propagation="REQUIRED" />
            <tx:method name="add*" propagation="REQUIRED" />
            <tx:method name="new*" propagation="REQUIRED" />
            <tx:method name="set*" propagation="REQUIRED" />
            <tx:method name="remove*" propagation="REQUIRED" />
            <tx:method name="delete*" propagation="REQUIRED" />
            <tx:method name="change*" propagation="REQUIRED" />
            <tx:method name="check*" propagation="REQUIRED" />
            <tx:method name="get*" propagation="REQUIRED" read-only="true" />
            <tx:method name="find*" propagation="REQUIRED" read-only="true" />
            <tx:method name="load*" propagation="REQUIRED" read-only="true" />
            <tx:method name="*" propagation="REQUIRED" read-only="true" />
        </tx:attributes>
    </tx:advice>

    <!-- 配置事务切面 -->
    <aop:config>
        <aop:pointcut id="serviceOperation" expression="execution(* cn.quartz.service.*.*(..))" />
        <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation" />
    </aop:config>

</beans>

表user

/* Navicat Premium Data Transfer Source Server : lllllll Source Server Type : MySQL Source Server Version : 50649 Source Host : localhost:3306 Source Schema : test Target Server Type : MySQL Target Server Version : 50649 File Encoding : 65001 Date: 08/03/2021 13:47:09 */

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `age` int(11) NULL DEFAULT NULL,
  `sex` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'hu', 22, '男');
INSERT INTO `user` VALUES (2, 'an', 23, '女');
INSERT INTO `user` VALUES (3, 'he', 22, '男');
INSERT INTO `user` VALUES (4, 'huan', 21, '男');
INSERT INTO `user` VALUES (11, 'huanmin', 22, '男');

SET FOREIGN_KEY_CHECKS = 1;

job

package cn.quartz.job;

import cn.quartz.pojo.User;
import cn.quartz.service.UserService;
import org.quartz.DisallowConcurrentExecution;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.List;

@Component
@DisallowConcurrentExecution
public class MyTask {

    @Autowired
    private UserService userService;

    @Scheduled(cron = "0/10 * * * * ?")  //每10秒执行一次
    public void excTask(){

        System.out.println("定时任务执行,执行时间是:"+new Date());
        List<User> users = userService.selectAll();
        System.out.println("查询数据库user表的全部值是:"+users);
    }
}

mapper

package cn.quartz.mapper;

import cn.quartz.pojo.User;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface UserMapper {

    @Select("select * from user")
    List<User> selectAll();

}

pojo

package cn.quartz.pojo;

import java.io.Serializable;

public class User implements Serializable {
    private Integer id;

    private String name;

    private Integer age;

    private String sex;

   ......get set toString
}

service

UserService

package cn.quartz.service;

import cn.quartz.pojo.User;

import java.util.List;

public interface UserService {

    List<User> selectAll();

}

impl/UserServiceImpl

package cn.quartz.service.impl;

import cn.quartz.mapper.UserMapper;
import cn.quartz.pojo.User;
import cn.quartz.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public List<User> selectAll() {
        return userMapper.selectAll();
    }
}

测试启动类

package cn.quartz;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
    public static void main(String[] args) {
        new ClassPathXmlApplicationContext("ApplicationContext.xml");
    }

}

测试结果这里就不贴上了 大概就是没10秒就从数据库查询一次user表内的全部数据 然后打印在控制台

定时器配合Redis使用

在上面案例的基础上我们增加Redis进行使用

我们来想一个案例就是秒杀商品 全部步骤之一中的商品录入

秒杀查询压力是非常大的,我们可以在秒杀之前把秒杀商品存入到Redis缓存中,页面每次列表查询的时候直接从Redis缓存中取,这样会大大减轻MySQL数据库的压力。我们可以创建一个定时任务工程,每天秒杀的前一天运行并加载MySQL数据库数据到Redis缓存。

项目结构:

图中utils里的工具类就不提供了还有和工具类相关的配置文件

因为Redis在java中使用方式有很多 所以为这里只提供思想

关键代码

package cn.quartz.job;

import cn.quartz.pojo.User;
import cn.quartz.service.UserService;
import cn.quartz.utils.JsonTurnUtils;
import cn.quartz.utils.RedisUtis;
import org.quartz.DisallowConcurrentExecution;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

@Component
@DisallowConcurrentExecution
public class MyTask {
    
    @Autowired
    private UserService userService;

    //每天23点59分钟 开始将数据库最新的数据录入到 Redis里
    @Scheduled(cron = "0 59 23 * * ?")
    public void excTask(){
        System.out.println("定时任务执行,执行时间是:"+new Date());
        List<User> users = userService.selectAll();
        System.out.println("查询数据库user表的全部值是:"+users);
        String josn= JsonTurnUtils.objTurnJson(users);
        RedisUtis.setStringRedis("userAll",josn);
    }

    //上面代码的测试版
    @Scheduled(cron = "0 0/1 * * * ?") //每一分钟录入一次
    public void excTask_test(){

        System.out.println("定时任务执行,执行时间是:"+new Date());
        List<User> users = userService.selectAll();
        System.out.println("查询数据库user表的全部值是:"+users);
        String josn= JsonTurnUtils.objTurnJson(users);
        RedisUtis.setStringRedis("userAll",josn);
        //记录录入的时间
        Date dNow = new Date( );
        SimpleDateFormat ft = new SimpleDateFormat ("yyyy-MM-dd hh:mm:ss");
        RedisUtis.setStringRedis("userAll_date", ft.format(dNow));
        System.out.println("数据录入到Redis成功");
    }
}

然后我们使用reids进行查询下

@Test
    public   void   getKey(){
        String userAll = RedisUtis.getStringRedis("userAll");
        System.out.println("数据: "+userAll);
        String userAll_date= RedisUtis.getStringRedis("userAll_date");
        System.out.println("录入数据的最后时间:"+userAll_date);
        
    }

数据: [{“id”:1,“name”:“hu”,“age”:22,“sex”:“男”},{“id”:2,“name”:“an”,“age”:23,“sex”:“女”},{“id”:3,“name”:“he”,“age”:22,“sex”:“男”},{“id”:4,“name”:“huan”,“age”:21,“sex”:“男”},{“id”:11,“name”:“huanmin”,“age”:22,“sex”:“男”}]
录入数据的最后时间:2021-03-08 04:14:00

小结: 使用Quartz定时器能干的事情太多太多了

比如:

  1. 定时生成报表
  2. 信用卡自动还款
  3. 定时给当年暗恋女神发一封匿名贺卡
  4. 想每隔1小时,备份一下自己的爱情动作片
  5. 订单支付来说,超过一定时间会执行这个job,去判断你是否支付,未支付就会取消此次订单
  6. 银行每天会自动下载流水
  7. 出于安全考虑,长时间没有修改密码会发出警告
  8. 会员到期了,TX提醒你进行充值

根据上面的这些案例 我们可以总结出 ,Quartz就是干自动化的 也就是当条件满足时自动完成一些任务而不依托于主程序

SpringBoot定时器

springboot框架已经默认集成了spring定时器,我们只需要开启,并实现我们的业务逻辑就可以了。
使用SpringBoot创建定时任务非常简单,目前主要有以下三种创建方式:

一、基于注解(@Scheduled)
二、基于接口(SchedulingConfigurer) 前者相信大家都很熟悉,但是实际使用中我们往往想从数据库中读取指定时间来动态执行定时任务,这时候基于接口的定时任务就派上用场了。
三、基于注解设定多线程定时任务

一、静态:基于注解

基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。

1、创建定时器
使用SpringBoot基于注解来创建定时任务非常简单,只需几行代码便可完成。 代码如下:

@Configuration      //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling   // 2.开启定时任务
public class SaticScheduleTask {
    //3.添加定时任务
    @Scheduled(cron = "0/5 * * * * ?")
    //或直接指定时间间隔,例如:5秒
    //@Scheduled(fixedRate=5000)
    private void configureTasks() {
        System.err.println("执行静态定时任务时间: " + LocalDateTime.now());
    }
}

cron 表达式,上面教程有自己查询

2、启动测试
启动应用,可以看到控制台打印出如下信息:

显然,使用@Scheduled 注解很方便,但缺点是当我们调整了执行周期的时候,需要重启应用才能生效,这多少有些不方便。为了达到实时生效的效果,可以使用接口来完成定时任务。

二、动态:基于接口

基于接口(SchedulingConfigurer)
1、导入依赖包:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>2.0.4.RELEASE</version>
    </parent>

    <dependencies>
        <dependency><!--添加Web依赖 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency><!--添加MySql依赖 -->
             <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency><!--添加Mybatis依赖 配置mybatis的一些初始化的东西-->
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency><!-- 添加mybatis依赖 -->
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

2、添加数据库记录:
开启本地数据库mysql,随便打开查询窗口,然后执行脚本内容,如下:

DROP DATABASE IF EXISTS `socks`;
CREATE DATABASE `socks`;
USE `SOCKS`;
DROP TABLE IF EXISTS `cron`;
CREATE TABLE `cron`  (
  `cron_id` varchar(30) NOT NULL PRIMARY KEY,
  `cron` varchar(30) NOT NULL  
);
INSERT INTO `cron` VALUES ('1', '0/5 * * * * ?');

然后在项目中的application.yml 添加数据源:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/socks
    username: root
    password: 123456

3、创建定时器
数据库准备好数据之后,我们编写定时任务,注意这里添加的是TriggerTask,目的是循环读取我们在数据库设置好的执行周期,以及执行相关定时任务的内容。
具体代码如下:

@Configuration      //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling   // 2.开启定时任务
public class DynamicScheduleTask implements SchedulingConfigurer {

    @Mapper
    public interface CronMapper {
        @Select("select cron from cron limit 1")
        public String getCron();
    }

    @Autowired      //注入mapper
    @SuppressWarnings("all")
    CronMapper cronMapper;

    /** * 执行定时任务. */
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {

        taskRegistrar.addTriggerTask(
                //1.添加任务内容(Runnable)
                () -> System.out.println("执行动态定时任务: " + LocalDateTime.now().toLocalTime()),
                //2.设置执行周期(Trigger)
                triggerContext -> {
                    //2.1 从数据库获取执行周期
                    String cron = cronMapper.getCron();
                    //2.2 合法性校验.
                    if (StringUtils.isEmpty(cron)) {
                        // Omitted Code ..
                    }
                    //2.3 返回执行周期(Date)
                    return new CronTrigger(cron).nextExecutionTime(triggerContext);
                }
        );
    }

}

4、启动测试
启动应用后,查看控制台,打印时间是我们预期的每10秒一次:


然后打开Navicat ,将执行周期修改为每6秒执行一次,如图:

查看控制台,发现执行周期已经改变,并且不需要我们重启应用,十分方便。如图:

注意: 如果在数据库修改时格式出现错误,则定时任务会停止,即使重新修改正确;此时只能重新启动项目才能恢复。

三、多线程定时任务

基于注解设定多线程定时任务

1、创建多线程定时任务

//@Component注解用于对那些比较中立的类进行注释;
//相对与在持久层、业务层和控制层分别采用 @Repository、@Service 和 @Controller 对分层中的类进行注释
@Component
@EnableScheduling   // 1.开启定时任务
@EnableAsync        // 2.开启多线程
public class MultithreadScheduleTask {

        @Async
        @Scheduled(fixedDelay = 1000)  //间隔1秒
        public void first() throws InterruptedException {
            System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
            System.out.println();
            Thread.sleep(1000 * 10);
        }

        @Async
        @Scheduled(fixedDelay = 2000)
        public void second() {
            System.out.println("第二个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
            System.out.println();
        }
    }

注: 这里的@Async注解很关键 ,不懂得可以百度或者在我博客里找答案,fixedDelay 也可以换成cron

2、启动测试
启动应用后,查看控制台:

从控制台可以看出,第一个定时任务和第二个定时任务互不影响;

并且,由于开启了多线程,第一个任务的执行时间也不受其本身执行时间的限制,所以需要注意可能会出现重复操作导致数据异常。

相关文章