参考:http://qinghua.github.io/spring-scheduler/
概念
spring scheduler 包含两个概念,任务(Task) 和 运行任务的框架(TaskExecutor/TaskScheduler)。TaskExecutor 是任务执行器,允许我们异步执行多个任务;TaskScheduler 是任务调度器,来运行定时任务;触发器Trigger 可以决定定时任务是否该运行了,最常用的是CronTrigger。
spring 内置了多种类型的 TaskExecutor 和TaskScheduler,方便用户根据不同的业务场景选择。
运行与配置
用xml 配置的话:
<?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:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd">
<task:scheduler id="myScheduler"/>
<task:scheduled-tasks scheduler="myScheduler">
<task:scheduled ref="doSomethingTask" method="doSomething" cron="0 * * * * *"/>
</task:scheduled-tasks>
</beans>
注意 doSomethingTask 以及对应的方法都必须存在
用注解的话
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class DoSomethingTask {
@Scheduled(cron="0 * * * * *")
public void doSomething() {
System.out.println("do something");
}
}
Cron
参考官方文档:http://www.quartz-scheduler.org/documentation/quartz-2.2.x/tutorials/crontrigger
Cron表达式由6~7项组成,中间用空格分开。从左到右依次是:秒、分、时、日、月、周几、年(可省略)。值可以是数字,也可以是以下符号:
*:所有值都匹配
?:无所谓,不关心,通常放在“周几”里
,:或者
/:增量值
-:区间
下面举几个例子,看了就知道了:
0 * * * * *:每分钟(当秒为0的时候)
0 0 * * * *:每小时(当秒和分都为0的时候)
*/10 * * * * *:每10秒
0 5/15 * * * *:每小时的5分、20分、35分、50分
0 0 9,13 * * *:每天的9点和13点
0 0 8-10 * * *:每天的8点、9点、10点
0 0/30 8-10 * * *:每天的8点、8点半、9点、9点半、10点
0 0 9-17 * * MON-FRI:每周一到周五的9点、10点…直到17点(含)
0 0 0 25 12 ?: 每年12约25日圣诞节的0点0分0秒(午夜)
0 30 10 * * ? 2016:2016年每天的10点半
其中的?在用法上其实和*是相同的。但是*语义上表示全匹配,而?并不代表全匹配,而是不关心。比如对于0 0 0 5 8 ? 2016来说,2016年8月5日是周五,?表示我不关心它是周几。而0 0 0 5 8 * 2016中的*表示周一也行,周二也行……语义上和2016年8月5日冲突了,你说谁优先生效呢。
不记得也没关系,记住Cron Maker也可以,它可以在线生成cron表达式。
配置技巧
不同的环境可能会配置不同的cron。
比如测试环境的cron 希望更频繁。
xml 配置:
<task:scheduler id="myScheduler"/>
<task:scheduled-tasks scheduler="myScheduler">
<task:scheduled ref="doSomethingTask" method="doSomething" cron="${cron_expression}"/>
<task:scheduled ref="doOtherThingTask" method="doOtherThing" cron="${cron_expression}"/>
</task:scheduled-tasks>
java 的properties 文件支持空格:
cron_expression=0 * * * * *
注解的解决方法:
@Scheduled(cron = "${cron_expression}")
注意事项
同一个task,如果前一个没有跑完,后面一个就不会触发,这没有问题。但是不同的task 也不能同时运行就不太合理了。
这个是由于schedule 的默认线程数被设置为1 的缘故。
解决方法1:
如下配置pool-size,但这样会导致同一个task 前一个还没有跑完后面又被触发的问题
<task:scheduler id="scheduler" pool-size="2" />
解决方法2:
让任务分别运行在不同的scheduler中
<task:scheduler id="myScheduler1"/>
<task:scheduler id="myScheduler2"/>
<task:scheduled-tasks scheduler="myScheduler1">
<task:scheduled ref="doSomethingTask" method="doSomething" cron="${0 * * * * *}"/>
</task:scheduled-tasks>
<task:scheduled-tasks scheduler="myScheduler2">
<task:scheduled ref="doOtherThingTask" method="doOtherThing" cron="${0 * * * * *}"/>
</task:scheduled-tasks>
多实例
若是scheduler与web配置在一起,在高可用的情况下,如果有多个web容器实例,scheduler会在多个实例上同时运行。
解决方法1:部署的时候,针对不同实例,使用不同的配置。比如tomcat_1打开scheduler,tomcat_2关闭。带来的问题是:
- 增加部署成本。
- 要是tomcat_1挂了,scheduler就不能运行了,高可用落空。
解决方法2:在task的基类加入一些逻辑,当开始运行时,将状态(运行机器的IP、时间等)写入数据库、缓存或者zk,运行结束时重置状态。其它实例看到有这样的数据,就直接返回。带来的问题是:
- 需要所有实例上的机器时间同步,不然一个刚结束另一个才开始,状态的维护就没有用了。
- 一定要保证结束运行后将状态重置,否则下一个运行周期,所有的task都会返回的。实在不行还得写一个task做这个事。
- 因为读写状态并非原子操作,偶尔也会发生task同时运行的事。
解决方法3:将scheduler与web分开。这样还能避免后台任务影响web端。带来的问题是:
- 增加部署成本。
- scheduler的高可用需要重新考虑。