前言
Spring Boot系列: 点击查看Spring Boot系列文章
定时任务
Spring Boot定时任务实现在主要有两种方法:
第一种:使用spring自带的Task任务。使用简单,功能比较单一
第二种:使用Quartz框架。功能强大,使用起来较复杂
以下主要介绍第一种方法,第一种方法对应一般普通的定时任务也够用了
Task开发
由于SpringBoot内置了定时任务Scheduled,所以我们不需要额外的导入依赖。
开启定时任务的方法有两种
一种是在配置类上加@EnableScheduling,然后在配置类中写定时任务方法,任务就会定时执行
下面这个是@EnableScheduling源码中的一个例子:
* @Configuration
* @EnableScheduling
* public class AppConfig {
*
* @Scheduled(fixedRate=1000)
* public void work() {
* // task execution logic
* }
* }
还有一种方法是在启动类上加@EnableScheduling注解,然后spring就会扫描该位置下定时任务并执行。现在一般是使用这种方法,但如果定时任务较少的话,使用第一种方法也可以。
@SpringBootApplication
@EnableScheduling
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
定义定时任务
下面我以第二种方法为例子,看如何定义定时任务
@Component
public class ScheduledTest {
private static final Logger logger = LoggerFactory.getLogger(ScheduledTest.class);
@Scheduled(fixedRate = 10000)
public void fixedRate() {
("fixedRate开始执行");
}
@Scheduled(fixedDelay = 10000)
public void fixedDelay() {
("fixedDelay开始执行");
}
@Scheduled(cron = "0/10 * * * * ?")
public void cron(){
("cron开始执行");
}
}
输出:
2020-06-17 14:24:28.626 INFO 18132 --- [ scheduling-1] c.e.d.c.scheduled.ScheduledTest : fixedRate开始执行
2020-06-17 14:24:28.628 INFO 18132 --- [ scheduling-1] c.e.d.c.scheduled.ScheduledTest : fixedDelay开始执行
2020-06-17 14:24:30.001 INFO 18132 --- [ scheduling-1] c.e.d.c.scheduled.ScheduledTest : cron开始执行
2020-06-17 14:24:38.626 INFO 18132 --- [ scheduling-1] c.e.d.c.scheduled.ScheduledTest : fixedRate开始执行
2020-06-17 14:24:38.630 INFO 18132 --- [ scheduling-1] c.e.d.c.scheduled.ScheduledTest : fixedDelay开始执行
2020-06-17 14:24:40.000 INFO 18132 --- [ scheduling-1] c.e.d.c.scheduled.ScheduledTest : cron开始执行
用@Component注解将定时类将给spring管理,这样就能扫描到并执行定时任务。 @Scheduled 注解表示开启一个定时任务,一个@Scheduled 注解的方法就表示了一个定时任务。
@Scheduled注解用来表示定时时间的属性有三个:fixedDelay,fixedRate,cron。
fixedRate 表示任务执行之间的时间间隔,具体是指两次任务的开始时间间隔,即定时任务开始时就开始计时,时间达到fixedRate 设置的时间值,就再次执行任务。
fixedDelay 表示任务执行之间的时间间隔,是从一个定时任务完成之后才开始计时的,达到fixedDelay 设定的值就再次执行,就是指本次任务结束到下次任务开始之间的时间间隔。
fixedRate 和fixedDelay 的区别就是fixedRate 是从一个任务开始执行就开始计时,而fixedDelay 是从一个任务执行完成才开始执行。在一个任务执行较久时,差别就很明显了。
注:fixedRate 和fixedDelay 的时间单位都是毫秒,1秒等于1000毫秒
cron表达式
cron属性对应一个cron表达式
cron表达式有六个或者七个域,每个域表示的时间单位如下:
秒 分钟 小时 日 月 星期 年
注意:在@Scheduled注解中的cron属性采用的是六个域的写法(即没有最后一个表示年的时间域),当输入七个的时候会报表达式错误
下面是一个cron说明表格
通配符含义:
? 表示不指定值,即不关心某个字段的取值时使用。需要注意的是,月份中的日期和星期可能会起冲突,因此在配置时这两个得有一个是 ?
* 表示所有值,例如:在秒的字段上设置 *,表示每一秒都会触发
, 用来分开多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发
- 表示区间,例如在秒上设置 “10-12”,表示 10,11,12秒都会触发
/ 用于递增触发,如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)
#序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六,(用 在母亲节和父亲节再合适不过了)
周字段的设置,若使用英文字母是不区分大小写的 ,即 MON 与mon相同
L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会自动判断是否是润年), 在周字段上表示星期六,相当于"7"或"SAT"(注意周日算是第一天)。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示"本月最后一个星期五"
W 表示离指定日期的最近工作日(周一至周五),例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发,如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,“W"前只能设置具体的数字,不允许区间”-")
L 和 W 可以一组合使用。如果在日字段上设置"LW",则表示在本月的最后一个工作日触发(一般指发工资 )
cron表达式在线生成网站
推荐一个在线生成cron表达式的网站:https://cron.qqe2.com/ 遇到不会写的cron表达式时,进去直接生成就可以了,妈妈再也不用担心不会写cron表达式了
定时任务的坑
Task单线程问题
从上面我的输出结果,可以看到每次的定时任务都是一个线程执行的(scheduling-1),这是因为spring提供的调度器是默认采用单线程的线程池
这样会导致如果一个定时任务发生阻塞,将会影响其他定时任务的执行,因此我们需要配置多线程执行来解决此问题。
下面这个例子说明但线程执行定时任务会出现说明什么坑
@Component
public class ScheduledTest {
private static final Logger logger = LoggerFactory.getLogger(ScheduledTest.class);
@Scheduled(fixedRate = 10000)
public void fixedRate() throws InterruptedException {
//让线程睡15秒
Thread.sleep(15000L);
("fixedRate开始执行");
}
@Scheduled(fixedDelay = 10000)
public void fixedDelay() {
("fixedDelay开始执行");
}
}
输出结果:
2020-06-17 15:02:04.424 INFO 4768 --- [ scheduling-1] c.e.d.c.scheduled.ScheduledTest : fixedRate开始执行
2020-06-17 15:02:04.425 INFO 4768 --- [ scheduling-1] c.e.d.c.scheduled.ScheduledTest : fixedDelay开始执行
2020-06-17 15:02:19.425 INFO 4768 --- [ scheduling-1] c.e.d.c.scheduled.ScheduledTest : fixedRate开始执行
2020-06-17 15:02:34.425 INFO 4768 --- [ scheduling-1] c.e.d.c.scheduled.ScheduledTest : fixedRate开始执行
2020-06-17 15:02:34.425 INFO 4768 --- [ scheduling-1] c.e.d.c.scheduled.ScheduledTest : fixedDelay开始执行
我们看结果,除了阻塞了任务,让任务不是指定时间执行的这个问题。还有一个大问题就是,大家看第二次执行定时任务,是少了fixedDelay开始执行这个定时任务输出的,说明fixedDelay这个定时任务没有执行成功。这是因为在第二次执行时,fixedRate睡眠了15秒,已经过了fixedDelay定时的10秒,所以第二个fixedDelay定时任务相当于没有执行。
那么我们就需要将定时任务变为并行执行,方法很简单,配置多个线程执行就可以了,添加配置类,实现SchedulingConfigurer 并覆写configureTasks方法,在该方法中我们将scheduledTask的单线程调度器换成我们自己创建的多线程调度器。具体线程数,要看自己的业务,我这里有两个定时任务,所以配置了两个线程。
@Configuration
public class ScheduledConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.setScheduler(Executors.newScheduledThreadPool(2));
}
}
再次执行上面的代码,输出以下结果
2020-06-17 15:17:40.230 INFO 15444 --- [pool-1-thread-2] c.e.d.c.scheduled.ScheduledTest : fixedDelay开始执行
2020-06-17 15:17:50.233 INFO 15444 --- [pool-1-thread-2] c.e.d.c.scheduled.ScheduledTest : fixedDelay开始执行
2020-06-17 15:17:55.230 INFO 15444 --- [pool-1-thread-1] c.e.d.c.scheduled.ScheduledTest : fixedRate开始执行
2020-06-17 15:18:00.234 INFO 15444 --- [pool-1-thread-2] c.e.d.c.scheduled.ScheduledTest : fixedDelay开始执行
2020-06-17 15:18:10.230 INFO 15444 --- [pool-1-thread-1] c.e.d.c.scheduled.ScheduledTest : fixedRate开始执行
2020-06-17 15:18:10.234 INFO 15444 --- [pool-1-thread-2] c.e.d.c.scheduled.ScheduledTest : fixedDelay开始执行
2020-06-17 15:18:20.235 INFO 15444 --- [pool-1-thread-2] c.e.d.c.scheduled.ScheduledTest : fixedDelay开始执行
从结果可以看出,两个定时任务已经分开执行,任务一不能阻塞任务二了。
有童鞋会担心,如果定时任务出错了,跑异常了,定时任务还会执行吗,下面我们就来试试
@Scheduled(fixedDelay = 10000)
public void fixedDelay() throws InterruptedException {
("fixedDelay开始执行");
throw new InterruptedException();
}
经过测试得知,当一个任务执行异常,是不影响下一次的执行,下一次还是会继续执行。