Spring Scheduler的使用与坑

时间:2021-08-17 02:16:57

http://qinghua.github.io/spring-scheduler/


Spring给程序猿们带来了许多便利。它包含了用于定时任务处理的Spring Scheduler。本文聊聊Scheduling模型和一些坑。

简介

Spring Scheduler里有两个概念:任务(Task)和运行任务的框架(TaskExecutor/TaskScheduler)。TaskExecutor顾名思义,是任务的执行器,允许我们异步执行多个任务。TaskScheduler是任务调度器,来运行未来的定时任务。触发器Trigger可以决定定时任务是否该运行了,最常用的触发器是CronTrigger,具体用法会在下面章节中详细介绍。Spring内置了多种类型的TaskExecutor和TaskScheduler,方便用户根据不同业务场景选择。

本文聊的是Spring Scheduler,所以我们接下来主要介绍Scheduler的用法。

运行

用xml配置Spring Scheduler的话,这样就行了:

12345678910111213
<?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>

当然需要能够reference到doSomethingTask这个实例了。

用注解配置Spring Scheduler的方式如下:

1234567891011
import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;@Componentpublic class DoSomethingTask {    @Scheduled(cron="0 * * * * *")    public void doSomething() {        System.out.println("do something");    }}

参考这篇短文可以迅速了解。

Cron

想了解Cron最好的方法是看Quartz的官方文档。本节也会大致介绍一下。

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配置的解决方法:

123456
<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文件支持value中带空格,所以可以放心地写cron_expression=0 * * * * *

注解的解决方法:

1
@Scheduled(cron = "${cron_expression}")

同时运行

同一个task,如果前一个还没跑完后面一个就不会触发,这没有问题。但是不同的task也不能同时运行就不太合理了。不过其实是scheduler的默认线程数为1的缘故。

解决方法1:如下配置pool-size,但这样会导致同一个task前一个还没跑完后面又被触发的问题。

1
<task:scheduler id="scheduler" pool-size="2" />

解决方法2:让任务分别运行在不同的scheduler里就好了。

12345678910
<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的高可用需要重新考虑。