定时任务在实际的开发中特别常见,比如电商平台 30 分钟后自动取消未支付的订单,以及凌晨的数据汇总和备份等,都需要借助定时任务来实现,本文记录了定时任务常见的几种实现方式。

转载文章:定时任务最简单的3种实现方法(Java)

参考文章:Java实现定时任务Quartz 定时任务(Scheduler)的 3 种实现方式

推荐文章:任务调度框架Quartz(三)任务调度框架Quartz实例详解深入理解Scheduler,Job,Trigger,JobDetail

Java 语言实现定时任务

在 Java 语言中,实现定时任务有几种常用的方法:

  1. java.util.Timer 类:这是Java标准库提供的一个类,可以用来安排任务以后在后台线程中执行。使用 Timer 类,你可以创建一个 TimerTask 任务,然后使用 schedule 或 scheduleAtFixedRate 方法来安排任务的执行。
  2. ScheduledExecutorService 接口:这是Java并发包中的一部分,提供了更灵活的定时任务调度能力。你可以使用 Executors 类创建一个 ScheduledExecutorService 实例,然后使用 schedule 或 scheduleAtFixedRate 方法来安排任务。
  3. Spring框架的 @Scheduled 注解:如果你在使用Spring框架,可以利用 @Scheduled 注解来简化定时任务的配置。Spring的调度器会根据注解的参数来执行相应的方法。
  4. Quartz Scheduler:这是一个开源的作业调度库,提供了比Java标准库更强大的定时任务功能。Quartz允许你配置复杂的调度策略,如cron表达式,并支持集群。

Timer

Timer 是 JDK 自带的定时任务执行类,无论任何项目都可以直接使用 Timer 来实现定时任务,所以 Timer 的优点就是使用方便.

定时任务的实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
public class MyTimerTask {
public static void main(String[] args) {
TimerTask timerTask = new TimerTask() { // 定义一个任务
@Override
public void run() {
log.info("Run timerTask:", new Date());
}
};
Timer timer = new Timer(); // 计时器
// 添加执行任务(延迟 1s 执行,每 3s 执行一次)
timer.schedule(timerTask, 1000, 3000);
}
}
1
2
3
Run timerTask:Mon Aug 17 21:29:25 CST 2020
Run timerTask:Mon Aug 17 21:29:28 CST 2020
Run timerTask:Mon Aug 17 21:29:31 CST 2020

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
@Slf4j
public class MyTimerTask {
public static void main(String[] args) {
TimerTask timerTask = new TimerTask() { // 定义任务 1
@Override
public void run() {
log.info("进入 timerTask 1:{}", new Date());
try {
TimeUnit.SECONDS.sleep(5); // 休眠 5 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("Run timerTask 1:{}", new Date());
}
};
TimerTask timerTask2 = new TimerTask() { // 定义任务 2
@Override
public void run() {
log.info("Run timerTask 2:{}", new Date());
}
};
Timer timer = new Timer(); // 计时器
// 添加执行任务(延迟 1s 执行,每 3s 执行一次)
timer.schedule(timerTask, 1000, 3000);
timer.schedule(timerTask2, 1000, 3000);
}
}
1
2
3
4
5
6
7
8
9
进入 timerTask 1:Mon Aug 17 21:44:08 CST 2020
Run timerTask 1:Mon Aug 17 21:44:13 CST 2020
Run timerTask 2:Mon Aug 17 21:44:13 CST 2020
进入 timerTask 1:Mon Aug 17 21:44:13 CST 2020
Run timerTask 1:Mon Aug 17 21:44:18 CST 2020
进入 timerTask 1:Mon Aug 17 21:44:18 CST 2020
Run timerTask 1:Mon Aug 17 21:44:23 CST 2020
Run timerTask 2:Mon Aug 17 21:44:23 CST 2020
进入 timerTask 1:Mon Aug 17 21:44:23 CST 2020

当任务 1 运行时间超过设定的间隔时间时,任务 2 也会延迟执行。原本任务 1 和任务 2 的执行时间间隔都是 3s,但因为任务 1 执行了 5s,因此任务 2 的执行时间间隔也变成了 10s(和原定时间不符)。

任务异常影响其他任务

使用 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
@Slf4j
public class MyTimerTask {
public static void main(String[] args) {
TimerTask timerTask = new TimerTask() { // 定义任务 1
@Override
public void run() {
log.info("进入 timerTask 1:{}", new Date());
// 模拟异常
int num = 8 / 0;
log.info("Run timerTask 1:{}", new Date());
}
};
TimerTask timerTask2 = new TimerTask() { // 定义任务 2
@Override
public void run() {
log.info("Run timerTask 2:{}", new Date());
}
};
Timer timer = new Timer(); // 计时器
// 添加执行任务(延迟 1s 执行,每 3s 执行一次)
timer.schedule(timerTask, 1000, 3000);
timer.schedule(timerTask2, 1000, 3000);
}
}
1
2
3
4
5
6
进入 timerTask 1:Mon Aug 17 22:02:37 CST 2020
Exception in thread "Timer-0" java.lang.ArithmeticException: / by zero
    at com.example.MyTimerTask$1.run(MyTimerTask.java:21)
    at java.util.TimerThread.mainLoop(Timer.java:555)
    at java.util.TimerThread.run(Timer.java:505)
Process finished with exit code 0

小结

Timer 类实现定时任务的优点是方便,因为它是 JDK 自定的定时任务,但缺点是任务如果执行时间太长或者是任务执行异常,会影响其他任务调度,所以在生产环境下建议谨慎使用。


ScheduledExecutorService

ScheduledExecutorService 也是 JDK 1.5 自带的 API,我们可以使用它来实现定时任务的功能,也就是说 ScheduledExecutorService 可以实现 Timer 类具备的所有功能,并且它可以解决了 Timer 类存在的所有问题。

实现定时任务的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
public class MyScheduledExecutorService {
public static void main(String[] args) {
// 创建任务队列
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(10);
scheduledExecutorService.scheduleAtFixedRate(() -> { // 执行任务 1
log.info("进入 Schedule:{}", new Date());
try {
TimeUnit.SECONDS.sleep(5); // 休眠 5 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("Run Schedule:{}", new Date());
}, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次

scheduledExecutorService.scheduleAtFixedRate(() -> { // 执行任务 2
log.info("Run Schedule2:{}", new Date());
}, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
}
}
1
2
3
Run Schedule:Mon Aug 17 21:44:23 CST 2020
Run Schedule:Mon Aug 17 21:44:26 CST 2020
Run Schedule:Mon Aug 17 21:44:29 CST 2020

可靠性测试

1、任务超时执行测试

首先,测试当一个任务执行时间过长,会不会对其他任务造成影响,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j
public class MyScheduledExecutorService {
public static void main(String[] args) {
// 创建任务队列
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(10);
scheduledExecutorService.scheduleAtFixedRate(() -> { // 执行任务 1
log.info("进入 Schedule:{}", new Date());
try {
TimeUnit.SECONDS.sleep(5); // 休眠 5 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("Run Schedule:{}", new Date());
}, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
scheduledExecutorService.scheduleAtFixedRate(() -> { // 执行任务 2
log.info("Run Schedule2:{}", new Date());
}, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
}
}
1
2
3
4
5
6
7
Run Schedule2:Mon Aug 17 11:27:55 CST 2020
进入 Schedule:Mon Aug 17 11:27:55 CST 2020
Run Schedule2:Mon Aug 17 11:27:58 CST 2020
Run Schedule:Mon Aug 17 11:28:00 CST 2020
进入 Schedule:Mon Aug 17 11:28:00 CST 2020
Run Schedule2:Mon Aug 17 11:28:01 CST 2020
Run Schedule2:Mon Aug 17 11:28:04 CST 2020

测试结果:当任务 1 执行时间 5s 超过了执行频率 3s 时,并没有影响任务 2 的正常执行。 所以 ScheduledExecutorService 可以解决 Timer 任务之间相应影响的缺点

2、任务异常测试

其次,测试在一个任务异常时,是否会对其他任务造成影响,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
public class MyScheduledExecutorService {
public static void main(String[] args) {
// 创建任务队列
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(10);
scheduledExecutorService.scheduleAtFixedRate(() -> { // 执行任务 1
log.info("进入 Schedule:{}", new Date());
// 模拟异常
int num = 8 / 0;
log.info("Run Schedule:{}", new Date());
}, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
scheduledExecutorService.scheduleAtFixedRate(() -> { // 执行任务 2
log.info("Run Schedule2:{}", new Date());
}, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
}
}
1
2
3
4
进入 Schedule:Mon Aug 17 22:17:37 CST 2020
Run Schedule2:Mon Aug 17 22:17:37 CST 2020
Run Schedule2:Mon Aug 17 22:17:40 CST 2020
Run Schedule2:Mon Aug 17 22:17:43 CST 2020

测试结果:当任务 1 出现异常时,并不会影响任务 2 的执行。

小结

在单机生产环境下建议使用 ScheduledExecutorService 来执行定时任务,它是 JDK 1.5 之后自带的 API,因此使用起来也比较方便,并且使用 ScheduledExecutorService 来执行任务,不会造成任务间的相互影响。


Spring Task

如果使用的是 Spring 或 Spring Boot 框架,可以直接使用 Spring Framework 自带的定时任务,使用上面两种定时任务的实现方式,很难实现设定了具体时间的定时任务,比如当我们需要每周五来执行某项任务时,但如果使用 Spring Task 就可轻松的实现此需求。 以 Spring Boot 为例,实现定时任务只需两步:

开启定时任务

开启定时任务只需要在 Spring Boot 的启动类上声明 @EnableScheduling 即可,实现代码如下:

1
2
3
4
5
@SpringBootApplication
@EnableScheduling // 启用定时任务支持
public class DemoApplication {
// do someing
}

添加定时任务

添加定时任务 定时任务的添加只需要使用 @Scheduled 注解标注即可,如果有多个定时任务可以创建多个 @Scheduled 注解标注的方法,示例代码如下:

1
2
3
4
5
6
7
8
9
@Slf4j
@Component // 把此类托管给 Spring,不能省略
public class TaskUtils {
// 添加定时任务
@Scheduled(cron = "59 59 23 0 0 5") // cron 表达式,每周五 23:59:59 执行
public void doTask(){
log.info("我是定时任务~");
}
}

注意:定时任务是自动触发的无需手动干预,也就是说 Spring Boot 启动后会自动加载并执行定时任务。

Cron表达式

Spring Task 的实现需要使用 cron 表达式来声明执行的频率和规则,cron 表达式是由 6 位或者 7 位组成的(最后一位可以省略),每位之间以空格分隔,表达式语法如下:

1
[秒] [分] [小时] [日] [月] [周] [年]

每位从左到右代表的含义如下表所示,其中* 和 ?号都表示匹配所有的时间。

字段 允许值 允许的特殊字符
秒(Seconds) 0~59的整数 , - * / 四个字符
分(Minutes) 0~59的整数 , - * / 四个字符
小时(Hours) 0~23的整数 , - * / 四个字符
日期(DayofMonth) 1~31的整数(但是你需要考虑你月的天数) , - * ? / L W C 八个字符
月份(Month) 1~12的整数或者 JAN-DEC , - * / 四个字符
星期(Dayofweek) 1~7的整数或者 SUN-SAT (1=SUN) , - * ? / L C # 八个字符
年(可选,留空)(Year) 1970~2099 , - * / 四个字符

cron 表达式在线生成地址:https://cron.qqe2.com/

通配符说明

  1. * 表示所有值。 例如:在分的字段上设置 *,表示每一分钟都会触发。
  2. ? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为”?” 具体设置为 0 0 0 10 * ?
  3. - 表示区间。例如 在小时上设置 “10-12”,表示 10,11,12点都会触发。
  4. , 表示指定多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发
  5. / 用于递增触发。如在秒上面设置”5/15” 表示从5秒开始,每增15秒触发(5,20,35,50)。 在月字段上设置’1/3’所示每月1号开始,每隔三天触发一次。
  6. L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于”7”或”SAT”。如果在”L”前加上数字,则表示该数据的最后一个。例如在周字段上设置”6L”这样的格式,则表示“本月最后一个星期五”
  7. W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上置”15W”,表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,”W”前只能设置具体的数字,不允许区间”-“)。
  8. # 序号(表示每月的第几个周几),例如在周字段上设置”6#3”表示在每月的第三个周六.注意如果指定”#5”,正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;小提示:’L’和 ‘W’可以一组合使用。如果在日字段上设置”LW”,则表示在本月的最后一个工作日触发;周字段的设置,若使用英文字母是不区分大小写的,即MON与mon相同。

常用Cron表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0/2 * * * * ?                         表示每2秒 执行任务
0 0/2 * * * ? 表示每2分钟 执行任务
0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务
0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 12 ? * WED 表示每个星期三中午12点
0 0 12 * * ? 每天中午12点触发
0 15 10 ? * * 每天上午10:15触发
0 15 10 * * ? 每天上午10:15触发
0 15 10 * * ? 每天上午10:15触发
0 15 10 * * ? 2005 2005年的每天上午10:15触发
0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发

cron表达式支持占位符

配置文件

1
2
3
time:
cron: */5 * * * * *
interval: 5

主程序

1
2
3
4
5
6
7
8
@Scheduled(cron="${time.cron}")
void task1() {
// 具体业务
}
@Scheduled(cron="*/${time.interval} * * * * *")
void task2() {
// 具体业务
}
  1. zone:时区,接收一个java.util.TimeZonecron表达式会基于该时区解析。默认是一个空字符串,即取服务器所在地的时区。比如我们一般使用的时区Asia/Shanghai。该字段一般留空。

  2. fixedDelay:上一次执行完毕时间点之后多长时间再执行。

    1
    @Scheduled(fixedDelay = 5000) // 上一次执行完毕时间点之后5秒再执行
  3. fixedDelayString:与 fixedDelay 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。

    1
    @Scheduled(fixedDelayString = "${time.fixedDelay}")
  4. fixedRate:上一次开始执行时间点之后多长时间再执行。

    1
    @Scheduled(fixedRate = 5000) // 上一次开始执行时间点之后5秒再执行
  5. fixedRateString:与 fixedRate 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。

  6. initialDelay: 第一次延迟多长时间后再执行。

    1
    @Scheduled(initialDelay=1000, fixedRate=5000) // 第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次
  7. initialDelayString: 与 initiaDelay 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。


Quartz Scheduler

Quartz Scheduler 是一个开源的作业调度库,提供了比Java标准库更强大的定时任务功能。Quartz允许配置复杂的调度策略,如cron表达式,并支持集群。Spring boot 官网文档:Quartz Scheduler ,推荐文章:Quartz-scheduler 定时器概述、核心 API 与 快速入门

引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- quartz 定时任务调度 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<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 定时器 https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-quartz -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>

实现方式一

  1. 定义好定时任务的业务内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import org.quartz.Job;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    import java.util.Date;

    @Slf4j
    @Service("show")
    public class Show implements Job {
    @Override
    public void execute(JobExecutionContext arg0) throws JobExecutionException {
    log.info("\n\n-------------------------------\n It is running and the time is : {}" +
    "\n-------------------------------\n", new Date());
    }
    }
  2. 声明定时任务,并关联业务实现类 。在 JobDetail jb = JobBuilder.newJob(Show.class) 中关联业务类

    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
    import org.quartz.*;
    import org.quartz.impl.StdSchedulerFactory;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    import java.util.Date;

    @Slf4j
    @Service("schedulerTest")
    public class SchedulerTest {
    public static void main(String[] args) {
    try {
    // 1.创建Scheduler的工厂
    SchedulerFactory sf = new StdSchedulerFactory();
    // 2.从工厂中获取调度器实例
    Scheduler scheduler = sf.getScheduler();
    // 3.创建JobDetail
    JobDetail jb = JobBuilder.newJob(Show.class) // Show 为一个job,是要执行的一个任务。
    .withDescription("这是我的测试定时任务。") // job的描述
    .withIdentity("jy2Job", "jy2Group") // job 的name和group
    .build();
    // 任务运行的时间,SimpleSchedle类型触发器有效
    long time = System.currentTimeMillis() + 3 * 1000L; // 3秒后启动任务
    Date statTime = new Date(time);
    // 4.创建Trigger
    // 使用SimpleScheduleBuilder或者CronScheduleBuilder
    Trigger t = TriggerBuilder.newTrigger()
    .withDescription("")
    .withIdentity("jyTrigger", "jyTriggerGroup")
    //.withSchedule(SimpleScheduleBuilder.simpleSchedule())
    .startAt(statTime) // 默认当前时间启动 ,也可以写为:.startNow();
    .withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?")) // 两秒执行一次
    .build();
    // 5.注册任务和定时器
    scheduler.scheduleJob(jb, t);
    // 6.启动 调度器
    scheduler.start();
    log.info("启动时间 :{}" , new Date());
    } catch (Exception e) {
    log.info("定时任务出现异常 : {}", e);
    }
    }
    }

实现方式二

  1. 定义好定时任务的业务内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import org.quartz.Job;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    import java.util.Date;

    @Slf4j
    @Service("show")
    public class Show implements Job {
    @Override
    public void execute(JobExecutionContext arg0) throws JobExecutionException {
    log.info("\n\n-------------------------------\n It is running and the time is : {}" +
    "\n-------------------------------\n", new Date());
    }

    }
  2. 定义好定时任务的触发类,调用业务类中的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import org.quartz.JobExecutionException;
    import javax.annotation.Resource;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import lombok.extern.slf4j.Slf4j;

    @Slf4j
    public class UserSyncTask {
    @Resource
    Show show;

    public void cronDepartmentsAndUsersJob() {
    log.info("\n\n 定时--开始,当前时间: {}", dateFormat().format(new Date()));
    try {
    show.execute(null);
    } catch (JobExecutionException e) {
    e.printStackTrace();
    }
    log.info("\n\n 定时--结束,当前时间:{}", dateFormat().format(new Date()));
    }
    private SimpleDateFormat dateFormat() {
    return new SimpleDateFormat("HH:mm:ss");
    }
    }
  3. 配置文件中:配置触发类和任务执行频率

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?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:util="http://www.springframework.org/schema/util"
    xmlns:task="http://www.springframework.org/schema/task"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
    <!--定时任务触发类-->
    <bean id="userSyncTask" class="gentle.test.UserSyncTask"></bean>
    <!--执行频率-->
    <task:scheduled-tasks>
    <!--每 2 秒执行一次-->
    <task:scheduled ref="userSyncTask" method="cronDepartmentsAndUsersJob" cron="0/2 * * * * ?" />
    </task:scheduled-tasks>
    </beans>

实现方式三

  1. 引入 jar , 同上

  2. 运行类,代码中给 2 个注解:

@EnableScheduling // 开启定时器

@Scheduled(fixedDelay = 2000) 或者 @Scheduled(cron = "* * 2 * * ?") // 每 2s 执行 1 次 。

  1. 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import gentle.util.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;

@Slf4j
@Component // 注册为一个bean
@EnableScheduling // 开启定时器
public class Sender {
@Scheduled(fixedDelay = 2000) // 每 2s 执行1次。
public void send() {
log.info(" \n------------------------\n 定时任务内容 :{}\n------------------------\n",
DateUtil.dateFormat().format(new Date()) );
}
}

拓展:分布式定时任务

以上都是关于单机定时任务的实现,如果是分布式环境可以使用 Redis 来实现定时任务。 使用 Redis 实现延迟任务的方法大体可分为两类:通过 ZSet 的方式和键空间通知的方式。

ZSet 实现方式

通过 ZSet 实现定时任务的思路是,将定时任务存放到 ZSet 集合中,并且将过期时间存储到 ZSet 的 Score 字段中,然后通过一个无线循环来判断当前时间内是否有需要执行的定时任务,如果有则进行执行,具体实现代码如下:

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
import redis.clients.jedis.Jedis;
import utils.JedisUtils;
import java.time.Instant;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class DelayQueueExample {
private static final String _KEY = "myTaskQueue"; // zset key

public static void main(String[] args) throws InterruptedException {
Jedis jedis = JedisUtils.getJedis();
long delayTime = Instant.now().plusSeconds(30).getEpochSecond(); // 30s 后执行
jedis.zadd(_KEY, delayTime, "order_1");
// 继续添加测试数据
jedis.zadd(_KEY, Instant.now().plusSeconds(2).getEpochSecond(), "order_2");
jedis.zadd(_KEY, Instant.now().plusSeconds(2).getEpochSecond(), "order_3");
jedis.zadd(_KEY, Instant.now().plusSeconds(7).getEpochSecond(), "order_4");
jedis.zadd(_KEY, Instant.now().plusSeconds(10).getEpochSecond(), "order_5");

doDelayQueue(jedis); // 开启定时任务队列
}

// 定时任务队列消费 @param jedis Redis 客户端
public static void doDelayQueue(Jedis jedis) throws InterruptedException {
while (true) {
Instant nowInstant = Instant.now(); // 当前时间
long lastSecond = nowInstant.plusSeconds(-1).getEpochSecond(); // 上一秒时间
long nowSecond = nowInstant.getEpochSecond();
// 查询当前时间的所有任务
Set<String> data = jedis.zrangeByScore(_KEY, lastSecond, nowSecond);
for (String item : data) {
// 消费任务
log.info("消费:{}", item);
}
jedis.zremrangeByScore(_KEY, lastSecond, nowSecond); // 删除已经执行的任务
Thread.sleep(1000); // 每秒查询一次
}
}
}

键空间通知

我们可以通过 Redis 的键空间通知来实现定时任务,它的实现思路是给所有的定时任务设置一个过期时间,等到了过期之后,我们通过订阅过期消息就能感知到定时任务需要被执行了,此时我们执行定时任务即可。 默认情况下 Redis 是不开启键空间通知的,需要我们通过 config set notify-keyspace-events Ex 的命令手动开启,开启之后定时任务的代码如下:

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
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import utils.JedisUtils;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TaskExample {
public static final String _TOPIC = "__keyevent@0__:expired"; // 订阅频道名称
public static void main(String[] args) {
Jedis jedis = JedisUtils.getJedis();
// 执行定时任务
doTask(jedis);
}

// 订阅过期消息,执行定时任务 @param jedis Redis 客户端
public static void doTask(Jedis jedis) {
// 订阅过期消息
jedis.psubscribe(new JedisPubSub() {
@Override
public void onPMessage(String pattern, String channel, String message) {
// 接收到消息,执行定时任务
log.info("收到消息:{}", message);
}
}, _TOPIC);
}
}

定时任务部署 xxl-job

参考文章链接

参考文章:SpringBoot整合Xxl-job实现定时任务Linux - Linux安装部署xxl-jobJava – XXL-JOB分布式任务调度平台

XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

单机并且定时任务不多时,可选择Timer注解@Scheduled或者Cron工具类等方式来实现,但是这时候定时任务会写死在代码中,一旦启动,就不能暂停或者修改。若需修改,整个项目都要重新编译。

部署xxl-job调度中心

(一)下载源码

xxl-job 源码地址:
GitHub - xuxueli/xxl-job: A distributed task scheduling framework.(分布式任务调度平台XXL-JOB)

2.4.1版本为例:https://github.com/xuxueli/xxl-job/archive/refs/tags/2.4.1.tar.gz

xxl-job 文档地址: 分布式任务调度平台XXL-JOB

(二)初始化数据库

找到 xxl-job 安装包xxl-job-2.4.1/doc/db/路径下的tables_xxl_job.sql文件,连接到mysql数据库,导入到mysql中

执行 sql 脚本后,会生成以下8张表:

1
2
3
4
5
6
7
8
xxl_job_group
xxl_job_info
xxl_job_lock
xxl_job_log
xxl_job_log_report
xxl_job_logglue
xxl_job_registry
xxl_job_user

(三)修改配置文件

  1. 修改server.port,服务端口号,修改数据库的URL和账号密码(xxl-job-admin 下的配置文件application.properties

    1
    2
    3
    4
    5
    6
    7
    8
    # 改成自己的服务端口号
    server.port=8666
    server.servlet.context-path=/xxl-job-admin
    # 修改数据库的URL和账号密码
    spring.datasource.url=jdbc:mysql:127.0;0.1:3306/xxl_job?useUnicode=true
    pring.datasource.username=root
    spring.datasource.password
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  2. 然后打包(lifecycle → clean → package)上传,并运行jar包,使服务运行起来

  3. 测试:地址栏输入http://IP或者`域名:8666/xxl-job-admin/`

    xxl-job-admin Web界面的账号:admin 密码:123456 (默认的情况)

    因为账号密码默认在代码里写死了,可在JobInfoControllerTestlogin方法修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class JobInfoControllerTest extends AbstractSpringMvcTest {
    private static Logger logger = LoggerFactory.getLogger(JobInfoControllerTest.class);

    private Cookie cookie;

    @BeforeEach
    public void login() throws Exception {
    MvcResult ret = mockMvc.perform(
    post("/login")
    .contentType(MediaType.APPLICATION_FORM_URLENCODED)
    .param("userName", "admin")
    .param("password", "123456")
    ).andReturn();
    cookie = ret.getResponse().getCookie(LoginService.LOGIN_IDENTITY_KEY);
    }
    }

部署SpringBoot项目

(一)引入Maven依赖

1
2
3
4
5
6
<!--    xxl-job依赖,maven仓库有该依赖,不需要手动安装   -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.4.0</version>
</dependency>

(二)修改项目关于xxl-job的配置文件

在springboot项目的 application-dev.yml 配置文件中新增 xxl-job 配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Xxl-Job分布式定时任务调度中心
xxl:
job:
admin:
# 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。
addresses: http://localhost:8666/xxl-job-admin
# addresses: http://IP:8666/xxl-job-admin
# 执行器通讯TOKEN [选填]:非空时启用 系统默认 default_token
accessToken: default_token
executor:
# 执行器的应用名称
appname: test-xxl-job
# 执行器注册 [选填]:优先使用该配置作为注册地址
address:
# 执行器IP [选填]:默认为空表示自动获取IP
ip:
# 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999
port: 9999
# 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
logpath: D:\Codes\logs
#logpath: /data/logs/mls/job
# 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
logretentiondays: 7

注意:若 accessToken 不设置最好注释掉,或输入xxl-job 默认的 default_token ,否则调用时易引起获取不到 token 的报错

1
xxl-job registry fail, registryParam:RegistryParam{registryGroup=‘EXECUTOR’, registryKey=‘xxl-job-invoice-api’, registryValue=‘http://xx:9990/’}, registryResult:ReturnT [code=500, msg=The access token is wrong., content=null]

另外需要注意的几个点

  1. xxl.job.admin.address是xxl-job-admin服务部署的IP地址

  2. 确认执行器的注册地址正确无误。检查两个地址是否均正确录入并与执行器实际部署的地址对应。

  3. 确认执行器的注册成功。可以在xxl-job-admin的”执行器管理“页面确认两个执行器均处于在线状态。

  4. 检查任务调度配置。在任务调度配置中,确保你的任务绑定了正确的执行器,并且任务的JobHandler名称与执行器中注册的JobHandler名称一致。

    • xxl-job-admin的”任务管理”页面,找到你的任务并编辑

    • 在任务编辑页面的”基本设置”中,确认”任务路由策略”选择了”单一机器”或”故障转移”,以便指定任务绑定的执行器

    • 在”GLUE类型”为Bean模式下,确保”JobHandler“字段的值与执行器中注册的 JobHandler 名称完全匹配

  5. 检查执行器配置。在执行器部署的服务器上,确认xxl.job.executor.appnamexxl.job.executor.addressxxl.job.executor.ip等属性的配置与xxl-job-admin上的执行器配置一致

    • 确认执行器的appname与xxl-job-admin中的执行器配置相同

    • 确认执行器的address和ip与xxl-job-admin中的执行器配置相同

    • 执行器即SpringBoot项目中带有@XxlJob("xxlJobTest")注解

    • 执行器的IP地址即SpringBoot项目中带有@XxlJob("xxlJobTest")注解部署服务的IP地址

(三)创建配置类

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
@Slf4j
@Configuration
public class XxlJobConfig {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;

@Value("${xxl.job.accessToken}")
private String accessToken;

@Value("${xxl.job.executor.appname}")
private String appname;

@Value("${xxl.job.executor.address}")
private String address;

@Value("${xxl.job.executor.ip}")
private String ip;

@Value("${xxl.job.executor.port}")
private int port;

@Value("${xxl.job.executor.logpath}")
private String logPath;

@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;

@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}

(四)创建执行器

1
2
3
4
5
6
7
8
9
@Slf4j
@Component
public class XxlJobTest {
@XxlJob("xxlJobTest")
public ReturnT<String> xxlJobTest(String date) {
log.info("---------xxlJobTest定时任务执行成功--------");
return ReturnT.SUCCESS;
}
}

通过调度中心进行任务调度

  1. 执行器管理–新增执行器

    根据 application-dev.yml 中 xxl-job 的配置,去填写appName,机器地址,自己命名执行器名称 。

  2. 任务管理–新增任务

    执行器选择已经新增的执行器,调度类型选择CRON,选择或输入 cron 设置定时任务执行的频率;运行模式选择BEAN,JobHandler填写执行器方法上@XxlJob注解中的value

  3. 任务调度中心发起任务调度

    可以在任务管理列表每个任务的操作栏选择执行一次、启动,选择启动之后会按照配置的执行频率执行任务。

配置告警邮件通知

项目上线后,调度任务执行失败,当无告警信息时,只有再数据出现错误,进行问题定位和分析时,才会知道执行结果信息,每次出现问题,不能及时的知晓对于运维而言,很不友好。但xxl-job中具备这个能力,具体配置项参考下面详细信息。

xxl-job-admin服务配置在application.properties中针对邮件告警配置项有如下信息:

1
2
3
4
5
6
7
8
9
10
11
### xxl-job, email
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxxx@qq.com
spring.mail.from=xxxx@qq.com
# 这里需要注意一点: spring.mail.password 不是设置密码,是设置 **授权码**
spring.mail.password=xxxxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory

这里的配置信息,是配置一个邮件发送方信息。也就是:出现告警信息时,邮件发送者。 在任务管理里面针对任务在报警邮件栏输入邮件地址,保存即可。