定时任务

JDK定时器

理论基础

小顶堆

像下面的Timer和定时任务线程池,底层都是小顶堆的结构。

堆是特殊的树,满足下面两个条件就是一个小顶堆:

  • 是一颗完全二叉树
  • 堆中的某个节点的值总是不大于其父节点的值

插入元素(定时任务):插入尾部,逐步上浮(与父节点比较,进行交换)

删除堆顶元素(执行定时任务):将尾部(最大的元素)放到堆顶,逐步下沉(与子节点比较,进行交换)

时间轮算法

小顶堆只适合相近时间内的小量任务,当执行时间相差过大或任务量很大时,添加新任务或执行堆顶任务时堆化的性能很低。

像Quartz或者其他复杂的定时任务框架,底层更多地会使用时间轮算法。

链表+数组实现

while-true-sleep;遍历数组,每个下标建立一个链表,链表节点中存储任务,遍历到就取出执行。

比如数组的长度为24,每一个数组元素中存储执行任务的链表,相对小顶堆性能有了很大提升,但依然存在问题,比如想在每个月的1号执行任务就不好实现,不够灵活。

round型时间轮

任务上记录一个round值,遍历到便将round值减1,为0时取出执行。

比如数组的长度为24,每个数组元素中存储执行任务的链表,链表节点中除了存储任务,还存储了round值,比如明天的任务就可以设置round为1,当第二遍遍历到时便可取出执行。

存在问题:每次需要遍历所有的任务,效率较低

分层时间轮

使用多个不同时间维度的轮:

  • 天轮:记录几点执行
  • 月轮:记录几号执行

月轮中匹配当前日期,若存在任务,就到天轮中遍历任务执行,达到几号几点执行任务的需求。

像Linux中定时任务的cron表达式就是典型的分层时间轮

Timer

Timer类中有几个属性需要注意:

//小顶堆,存放timeTask
private final TaskQueue queue = new TaskQueue();
//任务执行线程;死循环不断检查是否有任务需要执行
private final TimerThread thread = new TimerThread(queue);

特点:

  • 单线程执行任务,任务可能相互阻塞
  • 运行时异常会导致timer线程终止
  • 任务调度时基于绝对时间的,对系统时间敏感

使用实例:

public class TimeTest {
    public static void main(String[] args) {
        // 这里定时任务就已经启动了,但是队列中没有任务,所以一直等待
        Timer timer = new Timer();
        for (int i = 0; i < 2; i++) {
            // 添加任务
            // timer.schedule(new MyTimerTask("task" + i), 0, 2000);
            timer.scheduleAtFixedRate(new MyTimerTask("task" + i), 0, 2000);
        }
    }
}

class MyTimerTask extends TimerTask {
    private String name;

    public MyTimerTask(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        try {
            System.out.println(name + "执行时间:" + new Date());
            Thread.sleep(1000);
            System.out.println(name + "结束时间:" + new Date());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

schedulescheduleAtFixedRate异同:

相同点:

任务执行未超时,下次执行时间 = 上次执行开始时间 + period;

任务执行超时,下次执行时间 = 上次执行结束时间;

在任务执行未超时时,它们都是上次执行时间加上间隔时间,来执行下一次任务。而执行超时时,都是立马执行。

区别点:

schedule侧重保持间隔时间的稳定。
schedule 执行任务超时,第N个任务会在  N-1个任务执行完成后,不做period等待,立即启动执行;N任务执行未超时,N+1个任务会在N任务执行完成后,等待period,再执行。
即:schedule侧重时间间隔的稳定,错过了就错过了,后续按照新的节奏走。

scheduleAtFixedRate侧重保持执行频率的稳定。
scheduleAtFixedRate在任务执行超时后,不错period等待,立即执行下一个任务。并且后续任务会根据超时时长,不做period等待,立即执行后续的任务,直到追上设定的节奏后,再进行period等待执行后续任务。
即:scheduleAtFixedRate侧重的是频率的稳定,如果错过了,就取消period等待,努力追上设定好的节奏。

定时任务线程池

ScheduledThreadPoolExecutor

  • 使用多线程执行任务,不会相互阻塞

  • 如果线程失活,会创建新线程执行任务。(线程抛异常,任务会被丢弃,需要做捕获处理)

  • DalayedWorkQueue:小顶堆,无界队列

    • 在定时线程池中,最大线程数是没有意义的,核心线程数才有意义

    • 执行时间距离当前时间越近的任务在队列的前面

    • 用于添加ScheduleFutureTask(继承于FutureTask,实现RunnableScheduledFuture接口)

      • 提供异步执行的能力,并且可以返回执行时间
    • 线程池中的线程从DelayQueue中获取ScheduleFutureTask,然后执行任务

    • 实现了Delayed接口,可以通过getDelay方法获取延迟时间

  • Leader-Follower模式

    • 避免没必要的唤醒和阻塞的操作,节省资源

Leader-follower线程模型中每个线程有三种模式,leader,follower, processing。

在Leader-follower线程模型一开始会创建一个线程池,并且会选取一个线程作为leader线程,leader线程负责监听网络请求,其它线程为follower处于waiting状态,当leader线程接受到一个请求后,会释放自己作为leader的权利,然后从follower线程中选择一个线程进行激活,然后激活的线程被选择为新的leader线程作为服务监听,然后老的leader则负责处理自己接受到的请求(现在老的leader线程状态变为了processing),处理完成后,状态从processing转换为follower

SingleThreadScheduledExecutor

  • 单线程的ScheduledThreadPoolExecutor

    public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }
    

使用示例:

public class ScheduleThreadPoolTest {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
        for (int i = 0; i < 2; i++) {
            scheduledThreadPool.scheduleAtFixedRate(new MyTask("tesk-" + i), 0, 2, TimeUnit.SECONDS);
        }
    }
}

class MyTask implements Runnable {
    private String name;

    public MyTask(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        try {
            System.out.println(name + "执行时间:" + new Date());
            Thread.sleep(1000);
            System.out.println(name + "结束时间:" + new Date());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
//间隔是固定的,无论上一个任务是否完成
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit);
//间隔是不固定的,其会在周期任务的上一个任务执行完成后再开始计时,并在指定时间间隔之后才开始执行任务
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);

定时任务框架-Quartz

官网:Quartz Enterprise Job Scheduler (quartz-scheduler.org)

结构图:

c25d8afce8bbd8c976a39df7caf62208.png

  1. Job:封装为JobDetail设置属性

    • @DisallowConcurrentExecution:禁止并发地执行同一个job定义(JobDetail定义的)多个实例
    • @PersistJobDataAfterExecution:持久化JobDetail中的JobDataMap(对Trigger中的DataMap无效)
      • 如果一个任务不是持久化的,则当没有触发器关联它时,Quartz会从scheduler中删除它
    • 如果一个任务请求恢复,一般是该任务执行期间发生了系统奔溃或者其他关闭进程的操作,当服务再次启动的时候,会再次执行该任务,此时,JobExercutionContext.isRecovering()会返回true
  2. Trigger:触发器

    • 优先级

      • 同时触发的Trigger之间才会比较优先级
      • 如果Trigger是可恢复的,在恢复后再调度时,优先级不变
    • misfire:错过触发

      • 判断条件:

        • job到达触发时间时没有执行
        • 被执行的延迟时间超过了Quartz配置的misfire Threshold阈值
    • 产生原因:

      • 当job达到触发时间时,所有线程都被其他job占用,没有可用线程

        • 再job需要触发的时间点,scheduler停止了
        • job使用了@DisallowConcurrentExecution注解,job不能并发执行,当达到了下一个job执行点时,上一个任务还未完成
        • job指定了过去的开始执行时间,例如当前是8点,指定开始时间为7点
      • 策略:默认都使用MISFIRE_INSTRUCTION_SMART_POLICY;Quartz会根据Trigger的类型(SimpleTrigger或CronTrigger)和配置自动选择最合适的处理方式。

        • SimpleTrigger:具体时间,指定间隔重复执行

          • now*相关的策略,会立即执行第一个misfire的任务,同时会修改startTime和repeatCount,导致会重新计算finalFireTime,打乱原计划

          • next*相关的策略,不会立即执行misfire的任务,补充执行

      • CronTrigger:cron表达式

        • MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY:Trigger错过后忽略Misfire策略。例如当前时间是下午2点,而任务本应该在早上10点执行,Quartz框架会等待下一个触发时间,比如下午3点,然后执行任务。它不会考虑错过的10点触发时间,而是仅仅基于当前时间和下一个预定触发时间进行调度。
          • MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:Trigger错过后立即执行
        • MISFIRE_INSTRUCTION_DO_NOTHING:Trigger错过后不做任何处理
    • calendar:设置排除时间段

  3. Scheduler:调度器,基于Trigger的设定执行Job

    • SchedulerFactory:

      • 创建Scheduler
      • DirectSchedulerFactory:在代码中定制Scheduler
      • StdSchedulerFactory:读取classPah下的quartz.properties文件来实例化Scheduler
    • JobStore:存储运行时信息,包括Trigger、Schduler、JobDetail、业务锁等

      • RAMJobStore(内存实现)

      • JobStoreTX(JDBC,事务由Quartz管理)

      • JobStoreCMT(JDBC,使用容器事务)

      • ClusteredJobStore(集群实现)

      • TerracottaJobStore(Terracotta中间件)

    • ThreadPool

      • SimpleThreadPool
      • 自定义线程池
  4. JobDataMap:保存任务实例的状态信息

    • JobDetail:默认旨在Job被添加到调度程序(任务执行计划表)scheduler的时候,存储一次关于该任务的状态信息数据,可以使用注解@PersistJobDataAfterExecution注解标明在一个任务执行完毕之后就存储一次
    • Trigger:任务被多个触发器引用的时候,根据不同的触发时机,可以提供不同的输入条件

简单使用:

@PersistJobDataAfterExecution
public class MyJob implements Job {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 判断是否是恢复执行的任务
        boolean recovering = context.isRecovering();
        // 获取各自存储的数据
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        JobDataMap triggerMap = context.getTrigger().getJobDataMap();
        JobDataMap mergedMap = context.getMergedJobDataMap();
        // 框架自动调用set方法,结果为 trigger
        System.out.println("name:" + name);
        // 1
        System.out.println("jobDataMap:" + jobDataMap.get("count"));
        jobDataMap.put("count", jobDataMap.getIntValue("count") + 1);
        // 2
        System.out.println("mergedMap:" + mergedMap.get("count"));
    }
}
public class quartzTest {
    public static void main(String[] args) throws SchedulerException {
        // 创建一个HolidayCalendar对象,定义假期日期(1月1日)
        HolidayCalendar holidayCalendar = new HolidayCalendar();
        Calendar christmas = new GregorianCalendar();
        // 1月
        christmas.set(Calendar.MONTH, Calendar.JANUARY);
        // 1日
        christmas.set(Calendar.DAY_OF_MONTH, 1);
        holidayCalendar.addExcludedDate(christmas.getTime());

        JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
                .withIdentity("job1", "group1")
                // 存储数据
                .usingJobData("job", "jobDetail")
                .usingJobData("name", "jobDetail")
                .usingJobData("count", 1)
                .build();
        // 创建触发器
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "trigger1")
                .usingJobData("trigger", "trigger")
                .usingJobData("name", "trigger")
                .usingJobData("count", 2)
                // 设置触发器的启动时间
                .startNow()
                // 设置启动策略
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        // 设置延迟时间
                        .withIntervalInSeconds(2)
                        // 设置一直重复执行
                        .repeatForever())
                .modifiedByCalendar("holidayCalendar")
                .build();
        // StdSchedulerFactory使用配置文件创建调度器;这里使用默认的
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        // 注册Trigger和JobDetail到Scheduler
        scheduler.scheduleJob(jobDetail, trigger);
        // 将HolidayCalendar关联到Scheduler
        /*
        name(名称):Calendar 对象指定的唯一标识符。
        calendar(日历):用于定义排除特定日期和时间的规则。
        replace(替换):设置为 true,新的 Calendar 对象将替换掉已存在的。
            为 false,如果同名的 Calendar 对象已经存在,将会抛出 ObjectAlreadyExistsException 异常。
        updateTriggers(更新触发器):设置为 true,那么与 Calendar 相关联的所有 Trigger 对象(使用 modifiedByCalendar(name) 方法关联的 Trigger)将会被更新,以反映新的 Calendar 规则。
            如果设置为 false,只有在新建 Trigger 时才会使用新的 Calendar 规则,已存在的 Trigger 不会受到影响。
         */
        scheduler.addCalendar("holidayCalendar", holidayCalendar, false, false);
        // 启动
        scheduler.start();
    }
}

整合SpringBoot

依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.3</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
</dependencies>

数据库建表脚本:

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(200) NOT NULL,
    JOB_GROUP VARCHAR(200) 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(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    JOB_NAME  VARCHAR(200) NOT NULL,
    JOB_GROUP VARCHAR(200) 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(200) 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(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) 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(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    CRON_EXPRESSION VARCHAR(200) 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(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) 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(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) 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(200) 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(200) 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(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    INSTANCE_NAME VARCHAR(200) 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(200) NULL,
    JOB_GROUP VARCHAR(200) 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(200) 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-2.3.0-SNAPSHOT\src\org\quartz\impl\jdbcjobstore

配置文件:

#============================================================================
# 1. 基本配置
#============================================================================

# 调度标识名,集群中每一个实例都必须使用相同的名称。
# 可以为任意字符串,对于scheduler来说此值没有任何意义,但是可以区分同一系统中多个不同的实例。
# 如果使用了集群的功能,就必须对每一个实例使用相同的名称,这样使这些实例“逻辑上”是同一个scheduler。
org.quartz.scheduler.instanceName = SERVICEX-SCHEDULER-INSTANCE

# ID设置为自动获取,每一个实例不能相同。
# 可以为任意字符串,如果使用了集群的功能,SCHEDULER实例的值必须唯一,可以使用AUTO自动生成。
org.quartz.scheduler.instanceId = AUTO

# 默认值为false
org.quartz.scheduler.rmi.export = false

# 默认值为false
org.quartz.scheduler.rmi.proxy = false

# 默认false,若是在执行Job之前Quartz开启UserTransaction,此属性应该为true。 Job执行完毕,JobDataMap更新完(如果是StatefulJob)事务就会提交。
# 可以在JOB类上使用 @ExecuteInJTATransaction注解,以便在各自的JOB上决定是否开启JTA事务。
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false

# 一个SCHEDULER节点允许接收的TRIGGER的最大数,默认是1。值越大定时任务执行的越多,代价是集群节点之间的不均衡。
org.quartz.scheduler.batchTriggerAcquisitionMaxCount=1

#============================================================================
# 2. 调度器线程池配置
#============================================================================

# 线程池的实现类,一般使用SimpleThreadPool即可满足需求
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool

# 指定线程数无默认值,至少为1,指定该值后线程数量不会动态增加。
org.quartz.threadPool.threadCount = 5

# 线程的优先级(最大为java.lang.Thread.MAX_PRIORITY 10,最小为Thread.MIN_PRIORITY 1,默认为5)
org.quartz.threadPool.threadPriority = 5

# 加载任务代码的ClassLoader是否从外部继承
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

# 是否设置调度器线程为守护线程
org.quartz.scheduler.makeSchedulerThreadDaemon = true


#============================================================================
# 3. 作业存储配置
#============================================================================

# JDBC的存储方式
org.quartz.jobStore.class=org.springframework.scheduling.quartz.LocalDataSourceJobStore

# 数据库驱动,类似于Hibernate的dialect,用于处理DB之间的差异,StdJDBCDelegate能满足大部分的DB的使用。
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate

# 是否加入集群,当有多个Quartz实例在用同一套数据库时,必须设置为true。
org.quartz.jobStore.isClustered = true

# 检查集群节点状态的频率, 默认值是 15000(即15秒)
# 只用于设置了isClustered为true的时,才需要设置该项,用于实例上报信息给集群中的其他实例,这个值的大小会影响到侦测失败实例的敏捷度。
org.quartz.jobStore.clusterCheckinInterval = 5000

# 这是JobStore能处理的错过触发的TRIGGER的最大数量。处理太多则很快就会导致数据库中的表被锁定够长的时间,这样则会妨碍别的(还未错过触发)TRIGGER执行的性能。
org.quartz.jobStore.maxMisfiresToHandleAtATime = 1
org.quartz.jobStore.txIsolationLevelSerializable = true

# 设置这个参数为true会告诉Quartz从数据源获取连接后不要调用它的setAutoCommit(false)方法,大部分情况下驱动都要求调用setAutoCommit(false)。
org.quartz.jobStore.dontSetAutoCommitFalse = false
# 这必须是一个从LOCKS表查询一行并对这行记录加锁的SQL。假设没有设置,默认值如下。{0}会在运行期间被配置的TABLE_PREFIX所代替。
org.quartz.jobStore.selectWithLockSQL=SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE

# 设置调度引擎对触发器超时的忍耐时间 (单位毫秒)
org.quartz.jobStore.misfireThreshold = 12000
# 表的前缀,默认QRTZ_
org.quartz.jobStore.tablePrefix = QRTZ_

SpringBoot在2.5.6版本之后就删除了关于Quartz相关的依赖;在2.5.6及之前版本:

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

SpringBoot只需要配置数据源即可。

配置类:

@Configuration
public class SchedulerConfig {
    @Autowired
    private DataSource dataSource;

    @Bean
    public Scheduler scheduler() throws IOException {
        return schedulerFactoryBean().getScheduler();
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
        SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();
        factoryBean.setSchedulerName("MyScheduler");
        factoryBean.setDataSource(dataSource);
        // 设置配置文件
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(new ClassPathResource("/spring-quartz.properties"));
        propertiesFactoryBean.afterPropertiesSet();
        factoryBean.setQuartzProperties(propertiesFactoryBean.getObject());
        factoryBean.setTaskExecutor(schedulerThreadPool());
        //设置延迟时间
        factoryBean.setStartupDelay(0);
        return factoryBean;
    }

    @Bean
    public Executor schedulerThreadPool() {
        //ThreadPoolTaskExecutor是Spring框架提供的一个线程池实现,通常用于管理任务的执行。
        ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
        // 设置线程数为虚拟机可用的处理器数
        threadPool.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        threadPool.setMaxPoolSize(Runtime.getRuntime().availableProcessors());
        threadPool.setQueueCapacity(Runtime.getRuntime().availableProcessors());
        return threadPool;
    }
}

设置监听器,在SpringBoot启动时启动调度器:

@Component
public class StartApplicationListener implements ApplicationListener<ContextClosedEvent> {
    @Autowired
    private Scheduler scheduler;

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "trigger1");
        try {
            Trigger trigger = scheduler.getTrigger(triggerKey);
            if (Objects.isNull(trigger)) {
                trigger = TriggerBuilder.newTrigger()
                    .withIdentity(triggerKey)
                    .withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ? "))
                    .build();
                JobDetail jobDetail = JobBuilder.newJob(MyQuartzJob.class)
                    .withIdentity("job1", "group1")
                    .build();
                scheduler.scheduleJob(jobDetail, trigger);
                scheduler.start();
            }
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }
}

cron表达式最好还是用在线工具生成,手打容易漏空格

public class MyQuartzJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        System.out.println("现在的时间是:" + new Date());
    }
}

常用简单实现

使用两个注解便可以基本满足我们定时任务的需求:

  • @Scheduled
  • @EnableScheduling

实例:

@EnableScheduling
@SpringBootApplication
public class MyRun {
    public static void main(String[] args) {
        SpringApplication.run(MyRun.class, args);
    }
}
@Service
public class MyService {
    @Scheduled(fixedRate = 1000)
    public void test(){
        System.out.println("Hello!");
    }
}

@Scheduled的用法非常灵活,以下是一些使用示例:

@Scheduled(fixedRate = 60000): 每60秒执行一次。
@Scheduled(fixedDelay = 60000): 当前次任务执行完成后,延迟60秒后执行下一次任务。
@Scheduled(initialDelay = 10000, fixedRate = 60000): 首次延迟10秒,之后每60秒执行一次。
@Scheduled(cron = "0 * * * * ?"): 使用Cron表达式来定义更复杂的定时任务规则。
posted @ 2023-11-23 16:43  LemonPuer  阅读(30)  评论(0)    收藏  举报