定时任务
发表于:2023-10-03 | 分类: 框架
字数统计: 5.1k | 阅读时长: 22分钟 | 阅读量:

JDK定时器

理论基础

小顶堆

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

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

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

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

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

时间轮算法

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

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

链表+数组实现

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

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

round型时间轮

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

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

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

分层时间轮

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

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

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

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

Timer

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

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

特点:

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

使用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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异同:

相同点:

1
2
3
4
5
任务执行未超时,下次执行时间 = 上次执行开始时间 + period;

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

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

区别点:

1
2
3
4
5
6
7
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

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

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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);
}
}
}
1
2
3
4
//间隔是固定的,无论上一个任务是否完成
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:任务被多个触发器引用的时候,根据不同的触发时机,可以提供不同的输入条件

简单使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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"));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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

依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<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>

数据库建表脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#============================================================================
# 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及之前版本:

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

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

配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@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启动时启动调度器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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表达式最好还是用在线工具生成,手打容易漏空格

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

常用简单实现

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

  • @Scheduled
  • @EnableScheduling

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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的用法非常灵活,以下是一些使用示例:

1
2
3
4
@Scheduled(fixedRate = 60000): 每60秒执行一次。
@Scheduled(fixedDelay = 60000): 当前次任务执行完成后,延迟60秒后执行下一次任务。
@Scheduled(initialDelay = 10000, fixedRate = 60000): 首次延迟10秒,之后每60秒执行一次。
@Scheduled(cron = "0 * * * * ?"): 使用Cron表达式来定义更复杂的定时任务规则。
上一篇:
Shell
下一篇:
Git