计划任务
实际开发中我们可能经常遇到需要定时处理的业务逻辑,比如定时刷新缓存、定时发送邮件等。Spring框架中其实内置了一个定时任务的简单实现,通过@Scheduled注解我们就能够实现定时任务调度。
同类技术比较
除了Spring自带的定时任务模块,Quartz是另一个定时任务调度框架。相对而言@Scheduled的优势是使用非常简单,Spring项目中直接就可以使用,缺点是缺乏对分布式的支持,如果是分布式的项目依赖大量定时任务调度还是应该考虑用Quartz这类调度框架,这部分内容可以参考Quartz相关章节。
除了定时调度框架,对于类似“X小时后关闭订单”或是“调用失败后每隔X分钟重新调用”等类似需求,我们应该优先考虑是否可以用消息队列中的“延迟队列”功能代替。过多的使用定时任务会产生复杂的任务依赖问题,在这类功能中设置过多定时任务不是最佳实践,通常来说,一个功能需要的定时任务越多,系统稳定性就越差!
开启计划任务支持
对于传统Spring工程,我们需要在XML配置中开启计划任务支持,我们需要添加<task:annotation-driven />,此外如果需要支持异步任务,还需要配置TaskExecutor。
<?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:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
<context:component-scan base-package="com.gacfox.demo"/>
<task:annotation-driven executor="myTaskExecutor"/>
<bean id="myTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5"/>
<property name="maxPoolSize" value="10"/>
<property name="queueCapacity" value="100"/>
<property name="threadNamePrefix" value="Async-"/>
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy"/>
</property>
</bean>
</beans>
对于SpringBoot工程就比较简单了,在启动类上添加@EnableScheduling注解即可,SpringBoot内置支持TaskExecutor,我们无需显式配置。
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表达式
除了指定定时执行的间隔,更为复杂的场景我们可以使用cron表达式。下面例子中,我们设置每分钟从0秒开始每2秒执行一次。
@Scheduled(cron = "0/2 * * * * *")
要注意一点的是这里使用的cron表达式和Linux的crontab规则有些不同,Linux的crontab更像是一个简化版,最明显的区别是不支持秒级控制,如果把这里的cron表达式写进Linux的crontab会报语法错误,这里不要搞混了。
串行执行和并行执行
下面例子代码中,我们创建了taskA和taskB两个定时任务,但和之前不同的是它们被标注了@Async注解。
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会让不同的任务在新的线程中执行而不互相干扰。
实际上,添加@Async注解后Spring会默认寻找名为taskExecutor的Bean作为调度的执行器,不过它默认是SimpleAsyncTaskExecutor,这并不是一个真正的线程池,而是每次调度都创建新的线程。如果我们的调度场景比较简单使用它是没太大问题的,但如果是高并发场景,我们都通常需要创建自己的线程池。
分布式锁
如果我们的应用节点是多实例部署的,就不得不考虑分布式锁的问题。比如,我们的程序要定时给相关人员发送一封邮件,而应用节点部署了3个实例,如果没有分布式锁的考虑,这三个实例可能会造成在规定时间内,发出三封邮件,这显然是不行的。
分布式锁有很多种实现方式,MySQL的排他锁、Redis、Zookeeper等均能实现,这里就不多做介绍了。
常见问题:任务重复执行
对于传统Spring工程,我们知道,SpringMVC会初始化一个Web子容器,它和Spring的配置文件也通常是分开的,子容器中一般只定义和Web相关的内容,如ViewResolver等。如果将定时任务Bean在两个容器中重复加载,重复执行问题就出现了,这会造成所有Bean在Spring容器和SpringMVC容器中各初始化了一份。大多数情况即使这么配置程序也能恰好正常工作,但我们定时任务的Bean如果初始化两次,定时任务也会连续运行两次,一些诡异的系统问题就可能源于此。