项目中经常有定时任务的需求,比如定时刷新缓存,定时发送邮件等,@Scheduled
功能能够通过很简单的配置,实现定时任务调度。
除了Spring自带的@Scheduled
框架,Quartz
是另一个定时任务调度框架。相对而言@Scheduled
的优势是使用非常简单,Spring项目中直接就可以使用,缺点是缺乏对分布式的支持,如果是分布式的项目依赖大量定时任务调度,还是应该考虑用Quartz
。这部分内容可以参考Quartz
相关章节。
除了定时调度框架,对于类似“X小时后关闭订单”,或是“调用失败后每隔X分钟重新调用”等类似需求,我们应该优先考虑是否可以用消息队列中的“延迟队列”功能代替。过多的使用定时任务会产生复杂的任务依赖问题,一个功能需要的定时任务越多,系统稳定性越差!
首先我们要在工程中开启定时任务功能,SpringBoot工程中在启动类上添加@EnableScheduling
注解即可。
package com.gacfox.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
下面代码创建了一个最简单的定时任务,运行项目后,每隔1秒会在控制台打印一个包含线程ID的字符串。
package com.gacfox.demo;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class TestSchedule {
@Scheduled(fixedDelay = 1000L)
public void test() {
System.out.println("Scheduled on thread [" + Thread.currentThread().getId() + "]");
}
}
输出例子:
Scheduled on thread [16]
Scheduled on thread [16]
Scheduled on thread [16]
Scheduled on thread [16]
Scheduled on thread [16]
创建定时任务使用@Scheduled
注解就可以实现了,上面代码不一定非要放在Web容器中,只要我们的JVM进程没有退出,Spring就会分派线程执行定时任务。但是要注意,Spring容器需要启动,且当前类需要定义为Spring容器中的Bean。
指定时间间隔有两种方式:
fixedDelay
:上一次执行完成后延迟若干毫秒执行(比较常用)fixedRate
:定时若干毫秒执行除了指定定时执行的间隔,更为复杂的场景我们可以使用cron表达式。
例子,每分钟从0秒开始每2秒执行一次:
@Scheduled(cron = "0/2 * * * * *")
要注意一点的是,这里使用的cron表达式和Linux的crontab规则有些不同,Linux的crontab更像是一个简化版,最明显的区别是不支持秒级控制,如果把这里的cron表达式写进Linux的crontab会报语法错误,这里不要搞混了。
下面例子代码中,我们创建了taskA
和taskB
两个定时任务。
package com.gacfox.demo;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class TestSchedule {
@Async
@Scheduled(fixedDelay = 1000L)
public void taskA() {
System.out.println("Scheduled taskA on thread [" + Thread.currentThread().getId() + "]");
}
@Async
@Scheduled(fixedDelay = 1000L)
public void taskB() {
try {
Thread.sleep(5000L);
System.out.println("Scheduled taskB on thread [" + Thread.currentThread().getId() + "]");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
例子输出:
Scheduled taskA on thread [17]
Scheduled taskA on thread [19]
Scheduled taskA on thread [21]
Scheduled taskA on thread [23]
Scheduled taskA on thread [25]
Scheduled taskB on thread [18]
假设我们没有加上@Async
注解,两个任务是在同一线程中串行执行的,也就是说,尽管我们希望taskA
是每秒执行一次,但是它被taskB
阻塞了,不可能每秒执行。
而加上@Async
注解后,Spring会创建一个默认线程数为100
的线程池,不同的任务会尽量在新的线程中执行而不互相干扰。
如果我们的应用节点是多实例部署的,就不得不考虑分布式锁的问题。比如,我们的程序要定时给相关人员发送一封邮件,而应用节点部署了3个实例,如果没有分布式锁的考虑,这三个实例可能会造成在规定时间内,发出三封邮件,这显然是不行的。
分布式锁有很多种实现方式,MySQL的排他锁、Redis、Zookeeper等均能实现,这里就不多做介绍了。
对于老式Spring工程,我们知道,SpringMVC会初始化一个Web子容器,它和Spring的配置文件也通常是分开的,子容器中一般只定义和Web相关的内容,如ViewResolver等。
如果将定时任务Bean在两个容器中重复加载,重复执行问题就出现了,这会造成所有Bean在Spring容器和SpringMVC容器中各初始化了一份。大多数情况即使这么配置程序也能恰好正常工作,但我们定时任务的Bean如果初始化两次,定时任务也会连续运行两次,一些诡异的系统问题就可能源于此。