定时任务的实现方式

  • 方式1:基于 java.util.Timer 定时器,实现类似闹钟的定时任务
  • 方式2:使用 Quartz、elastic-job、xxl-job 等开源第三方定时任务框架,适合分布式项目应用。该方式的缺点是配置复杂。
  • 方式3:使用 Spring 提供的一个注解 @Schedule,开发简单,使用比较方便。

    java.util.Timer实现定时任务

    基于 java.util.Timer 定时器,实现类似闹钟的定时任务。 这种方式在项目中使用较少,参考如下Demo。 ```java import java.util.Date; import java.util.Timer; import java.util.TimerTask;

public class SpringbootAppApplication {

  1. /**
  2. * main方法
  3. * @param args
  4. */
  5. public static void main(String[] args) {
  6. SpringApplication.run(SpringbootAppApplication.class, args);
  7. System.out.println("Server is running ...");
  8. TimerTask timerTask = new TimerTask() {
  9. @Override
  10. public void run() {
  11. System.out.println("task run:"+ new Date());
  12. }
  13. };
  14. Timer timer = new Timer();
  15. timer.schedule(timerTask,10,3000);
  16. }

}

  1. <a name="JfULz"></a>
  2. ## ScheduledExecutorService实现定时任务
  3. 该方法类似 Timer ,参考如下Demo。
  4. ```java
  5. public class TestScheduledExecutorService {
  6. public static void main(String[] args) {
  7. ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
  8. /**
  9. * @param command the task to execute 任务体
  10. * @param initialDelay the time to delay first execution 首次执行的延时时间
  11. * @param period the period between successive executions 任务执行间隔
  12. * @param unit the time unit of the initialDelay and period parameters 间隔时间单位
  13. */
  14. service.scheduleAtFixedRate(()->System.out.println("task ScheduledExecutorService "+new Date()), 0, 3, TimeUnit.SECONDS);
  15. service.scheduleAtFixedRate(()->System.out.println("task ScheduledExecutorService "+new Date()), 0, 3, TimeUnit.SECONDS);
  16. }
  17. }

@Schedule实现定时任务

  1. 首先,在项目启动类上添加 @EnableScheduling 注解,开启对定时任务的支持。@EnableScheduling 注解的作用是发现注解 @Scheduled 的任务并在后台执行该任务。 ```java @SpringBootApplication @EnableScheduling public class ScheduledApplication {

    public static void main(String[] args) {

    1. SpringApplication.run(ScheduledApplication.class, args);

    }

}

  1. 2. 编写定时任务类和方法,定时任务类通过 Spring IOC 加载,使用 @Component 注解
  2. 2. 定时方法使用 @Scheduled注解。下述代码中,fixedRate long 类型,表示任务执行的间隔毫秒数,下面代码中的定时任务每 3 秒执行一次。
  3. ```java
  4. @Component
  5. public class ScheduledTask {
  6. @Scheduled(fixedRate = 3000)
  7. public void scheduledTask() {
  8. System.out.println("任务执行时间:" + LocalDateTime.now());
  9. }
  10. }
  1. 运行工程,项目启动和运行日志如下,可见每 3 秒打印一次日志执行记录
    1. Server is running ...
    2. 任务执行时间-ScheduledTask2020-06-23T18:02:14.747
    3. 任务执行时间-ScheduledTask2020-06-23T18:02:17.748
    4. 任务执行时间-ScheduledTask2020-06-23T18:02:20.746
    5. 任务执行时间-ScheduledTask2020-06-23T18:02:23.747

@Scheduled注解

在上面 Demo 中,使用了 @Scheduled(fixedRate = 3000) 注解来定义每过 3 秒执行的任务。对于 @Scheduled 的使用可以总结如下几种方式

  • @Scheduled(fixedRate = 3000) :上一次开始执行时间点之后 3 秒再执行(fixedRate 属性:定时任务开始后再次执行定时任务的延时(需等待上次定时任务完成),单位毫秒)
  • @Scheduled(fixedDelay = 3000) :上一次执行完毕时间点之后 3 秒再执行(fixedDelay 属性:定时任务执行完成后再次执行定时任务的延时(需等待上次定时任务完成),单位毫秒)
  • @Scheduled(initialDelay = 1000, fixedRate = 3000) :第一次延迟1秒后执行,之后按 fixedRate 的规则每 3 秒执行一次( initialDelay 属性:第一次执行定时任务的延迟时间,需配合 fixedDelay 或者 fixedRate 来使用)
  • @Scheduled(cron=”0 0 2 1 ? “) :通过 cron 表达式定义规则

    多线程执行定时任务

    创建多个定时任务,并打印线程名称,示例代码如下。
    import org.slf4j.LoggerFactory; @Component public class ScheduledTask { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ScheduledTask.class); @Scheduled(cron = “0/5 *”) public void scheduled(){ logger.info(“使用cron—-任务执行时间:{} 线程名称:{}”,LocalDateTime.now(),Thread.currentThread().getName()); } @Scheduled(fixedRate = 5000) public void scheduled1() { logger.info(“fixedRate—-任务执行时间:{} 线程名称:{}”,LocalDateTime.now(),Thread.currentThread().getName()); } @Scheduled(fixedDelay = 5000) public void scheduled2() { logger.info(“fixedDelay—-任务执行时间:{} 线程名称:{}”,LocalDateTime.now(),Thread.currentThread().getName()); } } 复制代码
    程序输出如下。
    2020-06-23 23:31:04.447 INFO 34069 : fixedRate—-任务执行时间:2020-06-23T23:31:04.447 线程名称:scheduling-1 2020-06-23 23:31:04.494 INFO 34069 : fixedDelay—-任务执行时间:2020-06-23T23:31:04.494 线程名称:scheduling-1 2020-06-23 23:31:05.004 INFO 34069 : 使用cron—-任务执行时间:2020-06-23T23:31:05.004 线程名称:scheduling-1 2020-06-23 23:31:09.445 INFO 34069 : fixedRate—-任务执行时间:2020-06-23T23:31:09.445 线程名称:scheduling-1 2020-06-23 23:31:09.498 INFO 34069 : fixedDelay—-任务执行时间:2020-06-23T23:31:09.498 线程名称:scheduling-1 2020-06-23 23:31:10.003 INFO 34069 : 使用cron—-任务执行时间:2020-06-23T23:31:10.003 线程名称:scheduling-1 2020-06-23 23:31:14.444 INFO 34069 : fixedRate—-任务执行时间:2020-06-23T23:31:14.444 线程名称:scheduling-1 2020-06-23 23:31:14.503 INFO 34069 : fixedDelay—-任务执行时间:2020-06-23T23:31:14.503 线程名称:scheduling-1 2020-06-23 23:31:15.002 INFO 34069 : 使用cron—-任务执行时间:2020-06-23T23:31:15.002 线程名称:scheduling-1 复制代码
    可以看到,3个定时任务都已经执行,并且使同一个线程中串行执行。当定时任务增多,如果一个任务卡死,会导致其他任务也无法执行。
    因此,需要考虑多线程执行定时任务的情况。
  1. 创建配置类:在传统的 Spring 项目中,我们可以在 xml 配置文件添加 task 的配置,而在 Spring Boot 项目中一般使用 config 配置类的方式添加配置,所以新建一个 AsyncConfig 类。在配置类中,使用 @EnableAsync 注解开启异步事件的支持。

package com.lbs0912.spring.demo.app; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; @Configuration @EnableAsync public class AsyncConfig { private int corePoolSize = 10; private int maxPoolSize = 200; private int queueCapacity = 10; @Bean public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(corePoolSize); executor.setMaxPoolSize(maxPoolSize); executor.setQueueCapacity(queueCapacity); executor.initialize(); return executor; } } 复制代码

  • @Configuration:表明该类是一个配置类
  • @EnableAsync:开启异步事件的支持
  1. 在定时任务的类或者方法上添加 @Async 注解,表示是异步事件的定时任务。

@Component @Async public class ScheduledTask { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ScheduledTask.class); @Scheduled(cron = “0/5 *”) public void scheduled(){ logger.info(“使用cron 线程名称:{}”,Thread.currentThread().getName()); } @Scheduled(fixedRate = 5000) public void scheduled1() { logger.info(“fixedRate—- 线程名称:{}”,Thread.currentThread().getName()); } @Scheduled(fixedDelay = 5000) public void scheduled2() { logger.info(“fixedDelay 线程名称:{}”,Thread.currentThread().getName()); } } 复制代码

  1. 运行程序,控制台输出如下,可以看到,定时任务是在多个线程中执行的。

2020-06-23 23:45:08.514 INFO 34824 : fixedRate—- 线程名称:taskExecutor-1 2020-06-23 23:45:08.514 INFO 34824 : fixedDelay 线程名称:taskExecutor-2 2020-06-23 23:45:10.005 INFO 34824 : 使用cron 线程名称:taskExecutor-3 2020-06-23 23:45:13.506 INFO 34824 : fixedRate—- 线程名称:taskExecutor-4 2020-06-23 23:45:13.510 INFO 34824 : fixedDelay 线程名称:taskExecutor-5 2020-06-23 23:45:15.005 INFO 34824 : 使用cron 线程名称:taskExecutor-6 2020-06-23 23:45:18.509 INFO 34824 : fixedRate—- 线程名称:taskExecutor-7 2020-06-23 23:45:18.511 INFO 34824 : fixedDelay 线程名称:taskExecutor-8 2020-06-23 23:45:20.005 INFO 34824 : 使用cron 线程名称:taskExecutor-9 2020-06-23 23:45:23.509 INFO 34824 : fixedRate—- 线程名称:taskExecutor-10 2020-06-23 23:45:23.511 INFO 34824 : fixedDelay 线程名称:taskExecutor-1 2020-06-23 23:45:25.005 INFO 34824 : 使用cron 线程名称:taskExecutor-2 2020-06-23 23:45:28.509 INFO 34824 : fixedRate—- 线程名称:taskExecutor-3 2020-06-23 23:45:28.512 INFO 34824 : fixedDelay 线程名称:taskExecutor-4 2020-06-23 23:45:30.005 INFO 34824 : 使用cron 线程名称:taskExecutor-5 2020-06-23 23:45:33.509 INFO 34824 : fixedRate—- 线程名称:taskExecutor-6 2020-06-23 23:45:33.513 INFO 34824 : fixedDelay 线程名称:taskExecutor-7 2020-06-23 23:45:35.005 INFO 34824 : 使用cron 线程名称:taskExecutor-8 … 复制代码

Quartz实现定时任务

Basic

Quartz 是一个开源项目,完全由 Java 开发,可以用来执行定时任务,类似于 java.util.Timer。但是相较于 Timer, Quartz 增加了很多功能

  • 持久性作业 - 即保持调度定时的状态
  • 作业管理 - 对调度作业进行有效的管理

Quartz 中可以划分出3个基本部分,如下图所示

  1. 任务:JobDetail,具体的定时任务
  2. 触发器:Trigger,包括 SimpleTrigger 和 CronTrigger。触发器负责触发定时任务,其最基本的功能是指定 Job 的执行时间,执行间隔,运行次数等。
  3. 调度器:Scheduler,进行调度,实现如何指定触发器去执行指定的任务。

image.png

Demo-1

  1. 添加依赖

添加 spring-boot-starter-quartz 依赖。
org.springframework.boot spring-boot-starter-quartz 复制代码
对于 SpringBoot 1.5.9 以下的版本,还需要添加如下依赖。
org.springframework spring-context-support 复制代码

  1. 创建定时任务类 JobQuartz,该类继承 QuartzJobBean,并重写 executeInternal 方法。

package com.lbs0912.spring.demo.app.quartz; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.scheduling.quartz.QuartzJobBean; import java.util.Date; / @author lbs / public class JobQuartz extends QuartzJobBean { / 执行定时任务 @param jobExecutionContext jobExecutionContext @throws JobExecutionException JobExecutionException / @Override protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println(“quartz task: “ + new Date()); } } 复制代码

  1. 创建配置类 QuartzConfig

package com.lbs0912.spring.demo.app.quartz; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.SimpleScheduleBuilder; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /* @author lbs */ @Configuration public class QuartzConfig { @Bean //创建JobDetail实例 public JobDetail testJobDetail(){ return JobBuilder.newJob(JobQuartz.class).withIdentity(“jobQuartz”).storeDurably().build(); } @Bean public Trigger testTrigger(){ SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(10) //设置时间周期单位秒 .repeatForever(); //构建Trigger实例,每隔10s执行一次 return TriggerBuilder.newTrigger().forJob(testJobDetail()) .withIdentity(“jobQuartz”) .withSchedule(scheduleBuilder) .build(); } } 复制代码

  1. 启动项目,查看日志输出。

Server is running … quartz task: Wed Jun 24 00:49:07 CST 2020 quartz task: Wed Jun 24 00:49:17 CST 2020 quartz task: Wed Jun 24 00:49:27 CST 2020 复制代码

Demo-2

  1. 添加依赖

org.springframework.boot spring-boot-starter-quartz 复制代码

  1. 创建一个任务 PrintWordsJob 类,实现 Job接口,重写 execute 方法。

import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Random; /* @author liubaoshuai1 */ public class PrintWordsJob implements Job { @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { String printTime = new SimpleDateFormat(“yy-MM-dd HH-mm-ss”).format(new Date()); System.out.println(“PrintWordsJob start at:” + printTime + “, prints: Hello Job-“ + new Random().nextInt(100)); } } 复制代码

  1. 创建一个调度器 Schedule,并在该类中创建触发器 Trigger 实例,执行任务。

import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SchedulerFactory; import org.quartz.SimpleScheduleBuilder; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.impl.StdSchedulerFactory; @SpringBootApplication @EnableScheduling public class SpringbootAppApplication { /* main方法 @param args */ public static void main(String[] args) throws SchedulerException, InterruptedException { System.out.println(“Server is running …”); // 1、创建调度器Scheduler SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); // 2、创建JobDetail实例,并与PrintWordsJob类绑定(Job执行内容) JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class) .withIdentity(“job1”, “group1”).build(); // 3、构建Trigger实例,每隔1s执行一次 Trigger trigger = TriggerBuilder.newTrigger().withIdentity(“trigger1”, “triggerGroup1”) .startNow()//立即生效 .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(1)//每隔1s执行一次 .repeatForever()).build();//一直执行 //4、执行 scheduler.scheduleJob(jobDetail, trigger); System.out.println(“————scheduler start ! ——————“); scheduler.start(); //睡眠 TimeUnit.MINUTES.sleep(1); scheduler.shutdown(); System.out.println(“————scheduler shutdown ! ——————“); } } 复制代码

  1. 运行程序,可以看到程序每隔 1s 会打印出内容,且在一分钟后结束。

11:05:27.551 [DefaultQuartzSchedulerWorker-9] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.job1 PrintWordsJob start at:20-06-24 11-05-27, prints: Hello Job-5 11:05:28.548 [DefaultQuartzScheduler_Worker-10] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.job1 PrintWordsJob start at:20-06-24 11-05-28, prints: Hello Job-56 11:05:29.548 [DefaultQuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.job1 PrintWordsJob start at:20-06-24 11-05-29, prints: Hello Job-82 11:05:29.550 [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler$_NON_CLUSTERED shutdown complete. ————scheduler shutdown ! —————— 复制代码

Quartz代码分析

下面结合 Demo-2 工程,对 Quartz 框架中的几个参数进行说明。

  • Job和JobDetail
  • JobExecutionContext
  • JobDataMap
  • Trigger、SimpleTrigger、CronTrigger

    Job

    Job 是 Quartz 中的一个接口,接口下只有 execute 方法,在这个方法中编写业务逻辑。
    package org.quartz; public interface Job { void execute(org.quartz.JobExecutionContext jobExecutionContext) throws org.quartz.JobExecutionException; } 复制代码

    JobDetail

    JobDetail 用来绑定 Job,为 Job 实例提供许多属性

  • name

  • group
  • jobClass
  • jobDataMap

JobDetail 绑定指定的 Job,每次 Scheduler 调度执行一个 Job 的时候,首先会拿到对应的 Job,然后创建该 Job 实例,再去执行 Job 中的 execute() 的内容。任务执行结束后,关联的 Job 对象实例会被释放,且会被 JVM GC 清除。
为什么设计成 JobDetail + Job,不直接使用 Job ?
JobDetail 定义的是任务数据,而真正的执行逻辑是在 Job 中。这是因为任务是有可能并发执行,如果 Scheduler 直接使用 Job,就会存在对同一个 Job 实例并发访问的问题。而 JobDetail + Job 方式,Sheduler 每次执行,都会根据 JobDetail 创建一个新的 Job 实例,这样就可以规避并发访问的问题。

JobExecutionContext

JobExecutionContext 中包含了 Quartz 运行时的环境以及 Job 本身的详细数据信息。 当 Schedule 调度执行一个 Job 的时候,就会将 JobExecutionContext 传递给该 Job 的 execute() 中,Job 就可以通过 JobExecutionContext 对象获取信息。
JobExecutionContext 提供的信息如下
image.png

Trigger

Trigger 是 Quartz 的触发器,会去通知 Scheduler 何时去执行对应 Job。

  • new Trigger().startAt() : 表示触发器首次被触发的时间
  • new Trigger().endAt() :表示触发器结束触发的时间

SimpleTrigger 可以实现在一个指定时间段内执行一次作业任务或一个时间段内多次执行作业任务。 CronTrigger 是基于日历的作业调度,而 SimpleTrigger 是精准指定间隔,所以相比 SimpleTrigger,CroTrigger 更加常用。CroTrigger 是基于 Cron 表达式的。

Cron表达式

Ref

是否必填 值以及范围 通配符
0-59 , - * /
0-59 , - * /
0-23 , - * /
1-31 , - * ? / L W
1-12 或 JAN-DEC , - * /
1-7 或 SUN-SAT , - * ? / L #
1970-2099 , - * /

需要说明的是,Cron 表达式中,“周” 是从周日开始计算的。“周” 域上的 1 表示的是周日,7 表示周六。

Cron中的通配符

  • , :指的是在两个以上的时间点中都执行。如果我在 “分” 这个域中定义为 8,12,35,则表示分别在第8分,第12分 第35分执行该定时任务。
    • :指定在某个域的连续范围。如果在 “时” 这个域中定义 1-6,则表示在 1 到 6 点之每小时都触发一次,等价于 1,2,3,4,5,6
    • :表示所有值,可解读为 “每”。 如果在 “日” 这个域中设置 *,表示每一天都会触发。
  • ? :表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如,要在每月的 8 号触发一个操作,但不关心是周几,我们可以这么设置 0 0 0 8 * ?
  • / :表示触发步进(step),”/“ 前面的值代表初始值( “*” 等同 “0”),后面的值代表偏移量,在某个域上周期性触发。比如 在 “秒” 上定义 5/10 表示从 第 5 秒开始,每 10 秒执行一次;而在 “分” 上则表示从 第 5 分钟开始,每 10 分钟执行一次。
  • L :表示英文中的 LAST 的意思,只能在 “日” 和 “周” 中使用。在 “日” 中设置,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年), 在 “周” 上表示周六,相当于 “7” 或 “SAT”。如果在 “L” 前加上数字,则表示该数据的最后一个。例如在 “周” 上设置 “7L” 这样的格式,则表示 “本月最后一个周六”。
  • W :表示离指定日期的最近那个工作日(周一至周五)触发,只能在 “日” 中使用且只能用在具体的数字之后。若在 “日” 上置 “15W”,表示离每月 15 号最近的那个工作日触发。假如 15 号正好是周六,则找最近的周五(14号)触发;如果 15 号是周未,则找最近的下周一(16号)触发;如果 15 号正好在工作日(周一至周五),则就在该天触发。如果是 “1W” 就只能往本月的下一个最近的工作日推不能跨月往上一个月推。
  • : 例如,A#B 表示每月的第 B 个周的周 A(周的计数从周日开始),只能作用于 “周” 上。例如 2#3 表示在每月的第 3 个周二,5#3 表示本月第 3 周的星期四。

注意,L 用在 “周” 这个域上,每周最后一天是周六。“周” 域上的 1 表示的是周日,7 表示周六,即每周计数是从周日开始的。

可视化工具

在上述可视化工具网站上,点击“反解析到UI”,可以看到定时任务最近5次运行时间,便于理解。
另外,在IDEA中,安装 Cron Description 插件也可以进行可视化语义展示,如下图所示,鼠标悬浮到Cron表达式上,即可看到可视化语义。
image.png

https://juejin.cn/post/6844904198752960519#heading-1