定时任务 通常会存在 中大型企业级 项目中,为了减小 服务器、数据库 的压力,每每会以 定时任务 的方式去完成某些业务逻辑。java
常见的就是 金融服务系统 推送回调,通常支付系统订单在没有收到成功的回调返回内容时会 持续性的回调,这种回调通常都是 定时任务 来完成。web
还有就是 报表的生成,咱们通常会在客户 访问量小 时完成这个操做,也能够采用 定时任务 来完成。spring
这是 Java
自带的 java.util.Timer
类,这个类容许调度一个名为 java.util.TimerTask
任务。使用这种方式可让你的程序按照某一个 频度 执行,但不能在 指定时间 运行。如今通常用的较少。数据库
JDK
自带的一个类,是基于 线程池 设计的定时任务类,每一个 调度任务 都会分配到 线程池 中的一个 线程 去执行。也就是说,任务是 并发执行,互不影响的。编程
Spring 3.0
之后自带的 Task
,支持 多线程 调度,能够将它当作一个 轻量级 的 Quartz
,并且使用起来比 Quartz
简单许多,可是适用于 单节点 的 定时任务调度。后端
这是一个 功能比较强大 的的调度器,可让你的程序在指定时间执行,也能够按照某一个频度执行,配置起来 稍显复杂。Quartz
功能强大,能够结合 数据库 作 持久化,进行 分布式 的 任务延时调度。缓存
Cron
表达式是一个字符串,字符串以 5
或 6
个 空格 隔开,分为 6
或 7
个 域,每个域表明一个含义,Cron
有以下两种语法格式:springboot
- Seconds Minutes Hours DayofMonth Month DayofWeek Year
- Seconds Minutes Hours DayofMonth Month DayofWeek
每一个域对应的含义、域值范围和特殊表示符,从左到右依次以下:bash
字段 | 容许值 | 容许的特殊字符 |
---|---|---|
秒 | 0-59 | , - * / |
分 | 0-59 | , - * / |
小时 | 0-23 | , - * / |
日期 | 1-31 | , - * / L W C |
月份 | 1-12 或者 JAN-DEC | , - * / |
星期 | 1-7 或者 SUN-SAT | , - * / L C # |
年(可选) | 留空, 1970-2099 | , - * / |
如上面的表达式所示:服务器
""字符: 被用来指定全部的值。如:在分钟的字段域里表示"每分钟"。
"-"字符: 被用来指定一个范围。如:"10-12" 在小时域意味着 "10点、11点、12点"。
","字符: 被用来指定另外的值。如:"MON,WED,FRI" 在星期域里表示 "星期1、星期3、星期五"。
"?"字符: 只在日期域和星期域中使用。它被用来指定"非明确的值"。当你须要经过在这两个域中的一个来指定一些东西的时候,它是有用的。看下面的例子你就会明白。
"L"字符: 指定在月或者星期中的某天(最后一天)。即 "Last" 的缩写。可是在星期和月中 "L" 表示不一样的意思,如:在月子段中 "L" 指月份的最后一天 - 1月31日,2月28日。
"W"字符: 只能用在月份字段中,该字段指定了离指定日期最近的那个星期日。
"#"字符: 只能用在星期字段,该字段指定了第几个星期 value 在某月中
每个元素均可以显式地规定一个值(如 6
),一个区间(如 9-12
),一个列表(如 9,11,13
)或一个通配符(如 *
)。"月份中的日期" 和 "星期中的日期" 这两个元素是 互斥的,所以应该经过设置一个 问号(?
)来代表你不想设置的那个字段。下表显示了一些 cron
表达式的 例子 和它们的意义:
表达式 | 意义 |
---|---|
"0 0 12 * * ?" | 天天中午12点触发 |
"0 15 10 ? * *" | 天天上午10:15触发 |
"0 15 10 * * ?" | 天天上午10:15触发 |
"0 15 10 * * ? *" | 天天上午10:15触发 |
"0 15 10 * * ? 2005" | 2005年的天天上午10:15触发 |
"0 * 14 * * ?" | 在天天下午2点到下午2:59期间的每1分钟触发 |
"0 0/5 14 * * ?" | 在天天下午2点到下午2:55期间的每5分钟触发 |
"0 0/5 14,18 * * ?" | 在天天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 |
"0 0-5 14 * * ?" | 在天天下午2点到下午2:05期间的每1分钟触发 |
"0 10,44 14 ? 3 WED" | 每一年三月的星期三的下午2:10和2:44触发 |
"0 15 10 ? * MON-FRI" | 周一至周五的上午10:15触发 |
"0 15 10 15 * ?" | 每个月15日上午10:15触发 |
"0 15 10 L * ?" | 每个月最后一日的上午10:15触发 |
"0 15 10 ? * 6L" | 每个月的最后一个星期五上午10:15触发 |
"0 15 10 ? * 6L 2002-2005" | 2002年至2005年的每个月的最后一个星期五上午10:15触发 |
"0 15 10 ? * 6#3" | 每个月的第三个星期五上午10:15触发 |
0 6 * * * | 天天早上6点 |
0 /2 * * | 每两个小时 |
0 23-7/2,8 * * * | 晚上11点到早上8点之间每两个小时,早上八点 |
0 11 4 * 1-3 | 每月的4号和每一个礼拜的礼拜一到礼拜三的早上11点 |
0 4 1 1 * | 1月1日早上4点 |
利用 Spring Initializer
建立一个 gradle
项目 spring-boot-scheduler-task-management
,建立时添加相关依赖。获得的初始 build.gradle
以下:
buildscript {
ext {
springBootVersion = '2.0.3.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'io.ostenant.springboot.sample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter')
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
复制代码
在 Spring Boot
入口类上配置 @EnableScheduling
注解开启 Spring
自带的定时处理功能。
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
复制代码
这个 API
目前在项目中不多用,直接给出示例代码。具体的介绍能够查看 API
。Timer
的内部只有 一个线程,若是有 多个任务 的话就会 顺序执行,这样任务的 延迟时间 和 循环时间 就会出现问题。
TimerService.java
public class TimerService {
private static final Logger LOGGER = LoggerFactory.getLogger(TimerService.class);
private AtomicLong counter = new AtomicLong();
public void schedule() {
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
long count = counter.incrementAndGet();
LOGGER.info("Schedule timerTask {} times", count);
}
};
Timer timer = new Timer();
timer.schedule(timerTask, 1000L, 10 * 1000L;
}
}
复制代码
上面的代码定义了一个 TimerTask
,在 TimerTask
中累加 执行次数,并经过 slf4j
进行打印 (自带执行时间)。而后经过 Timer
调度工具类调度 TimerTask
任务,设置 初始化延迟时间 为 1s
,定时执行间隔 为 10s
,测试代码以下:
public static void main(String[] args) {
TimerService timerService = new TimerService();
timerService.schedule();
}
复制代码
观察测试结果,可以发现 TimerTask
配置的任务每隔 10s
被执行了一次,执行线程默认都是 Timer-0
这个线程。
17:48:18.731 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 1 times
17:48:28.730 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 2 times
17:48:38.736 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 3 times
17:48:48.738 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 4 times
17:48:58.743 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 5 times
复制代码
ScheduledExecutorService
是 延时执行 的线程池,对于 多线程 环境下的 定时任务,推荐用 ScheduledExecutorService
代替 Timer
定时器。
建立一个线程数量为 4
的 任务线程池,同一时刻并向它提交 4
个定时任务,用于测试延时任务的 并发处理。执行 ScheduledExecutorService
的 scheduleWithFixedDelay()
方法,设置任务线程池的 初始任务延迟时间 为 2
秒,并在上一次 执行完毕时间点 以后 10
秒再执行下一次任务。
public void scheduleWithFixedDelay() {
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(4);
for (int i = 0; i < 4; i++) {
scheduledExecutor.scheduleWithFixedDelay(() -> {
try {
TimeUnit.MILLISECONDS.sleep(10 * 1000L);
} catch (InterruptedException e) {
LOGGER.error("Interrupted exception", e);
}
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with fixed delay", count);
}, 2000L, 10 * 1000L, TimeUnit.MILLISECONDS);
}
LOGGER.info("Start to schedule");
}
复制代码
测试结果以下,咱们能够发现每隔 20
秒的时间间隔,就会有 4
个定时任务同时执行。由于在任务线程池初始化时,咱们同时向线程池提交了 4
个任务,这 四个任务 会彻底利用线程池中的 4
个线程进行任务执行。
20
秒是怎么来的?首先每一个任务的 时间间隔 设置为 10
秒。其次由于采用的是 withFixedDelay
策略,即当前任务执行的 结束时间,做为下次延时任务的 开始计时节点,而且每一个任务在执行过程当中睡眠了 10
秒的时间,累计起来就是 20
秒的时间。
19:42:02.444 [main] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Start to schedule
19:42:14.449 [pool-1-thread-1] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 3 times with fixed delay
19:42:14.449 [pool-1-thread-2] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 1 times with fixed delay
19:42:14.449 [pool-1-thread-3] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 2 times with fixed delay
19:42:14.449 [pool-1-thread-4] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 4 times with fixed delay
19:42:34.458 [pool-1-thread-4] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 7 times with fixed delay
19:42:34.458 [pool-1-thread-3] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 5 times with fixed delay
19:42:34.458 [pool-1-thread-2] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 8 times with fixed delay
19:42:34.458 [pool-1-thread-1] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 6 times with fixed delay
复制代码
建立一个线程数量为 4
的 任务线程池,同一时刻并向它提交 4
个定时任务,用于测试延时任务的 并发处理。每一个任务分别执行 ScheduledExecutorService
的 scheduleAtFixedRate()
方法,设置任务线程池的 初始任务延迟时间 为 2
秒,并在上一次 开始执行时间点 以后 10
秒再执行下一次任务。
public void scheduleAtFixedRate() {
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(4);
for (int i = 0; i < 4; i++) {
scheduledExecutor.scheduleAtFixedRate(() -> {
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times at fixed rate", count);
}, 2000L, 10 * 1000L, TimeUnit.MILLISECONDS);
}
LOGGER.info("Start to schedule");
}
复制代码
测试结果以下,咱们能够发现每隔 10
秒的时间间隔,就会有 4
个定时任务同时执行,由于在任务线程池初始化时,咱们同时向线程池提交了 4
个任务,这 四个任务 会彻底利用线程池中的 4
个线程进行任务执行。
19:31:46.837 [main] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Start to schedule
19:31:48.840 [pool-1-thread-1] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 1 times at fixed rate
19:31:48.840 [pool-1-thread-3] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 3 times at fixed rate
19:31:48.840 [pool-1-thread-2] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 2 times at fixed rate
19:31:48.840 [pool-1-thread-4] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 4 times at fixed rate
19:31:58.839 [pool-1-thread-2] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 6 times at fixed rate
19:31:58.840 [pool-1-thread-4] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 8 times at fixed rate
19:31:58.839 [pool-1-thread-3] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 7 times at fixed rate
19:31:58.839 [pool-1-thread-1] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 5 times at fixed rate
复制代码
Spring
提供了 @Scheduled
注解来实现 定时任务,@Scheduled
参数能够接受 两种 定时的设置,一种是咱们经常使用的 格林时间表达式 cron = "*/10 * * * * *"
,另外一种是 fixedRate = 10 * 1000L
,两种都表示每隔 10
秒执行一次目标任务。
参数说明:
10
秒再执行。@Scheduled(fixedRate = 10 * 1000L)
public void scheduleAtFixedRate() throws Exception {
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times at fixed rate", count);
}
复制代码
10
秒再执行。@Scheduled(fixedDelay = 10 * 1000L)
public void scheduleWithFixedDelay() throws Exception {
try {
TimeUnit.MILLISECONDS.sleep(10 * 1000L);
} catch (InterruptedException e) {
LOGGER.error("Interrupted exception", e);
}
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with fixed delay", count);
}
复制代码
2
秒后执行,以后按 fixedRate
的规则每 10
秒执行一次。@Scheduled(initialDelay = 2000L, fixedDelay = 10 * 1000L)
public void scheduleWithinitialDelayAndFixedDelay() throws Exception {
try {
TimeUnit.MILLISECONDS.sleep(10 * 1000L);
} catch (InterruptedException e) {
LOGGER.error("Interrupted exception", e);
}
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with fixed delay", count);
}
复制代码
cron
表达式定义,每隔 10
秒执行一次。@Scheduled(cron = "0/10 * * * * *")
public void scheduleWithCronExpression() throws Exception {
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with ", count);
}
复制代码
完整的代码以下:
SpringTaskService.java
@Component
public class SpringTaskService {
private static final Logger LOGGER = LoggerFactory.getLogger(SpringTaskService.class);
private AtomicLong counter = new AtomicLong();
@Scheduled(fixedDelay = 10 * 1000L)
public void scheduleWithFixedDelay() throws Exception {
try {
TimeUnit.MILLISECONDS.sleep(10 * 1000L);
} catch (InterruptedException e) {
LOGGER.error("Interrupted exception", e);
}
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with fixed delay", count);
}
@Scheduled(initialDelay = 2000L, fixedDelay = 10 * 1000L)
public void scheduleWithinitialDelayAndFixedDelay() throws Exception {
try {
TimeUnit.MILLISECONDS.sleep(10 * 1000L);
} catch (InterruptedException e) {
LOGGER.error("Interrupted exception", e);
}
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with fixed delay", count);
}
@Scheduled(fixedRate = 10 * 1000L)
public void scheduleAtFixedRate() throws Exception {
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times at fixed rate", count);
}
@Scheduled(cron = "0/10 * * * * *")
public void scheduleWithCronExpression() throws Exception {
long count = counter.incrementAndGet();
LOGGER.info("Schedule executor {} times with ", count);
}
}
复制代码
查看日志,任务每 20
秒的时间间隔执行一次。每次定时任务在上次 执行完毕时间点 以后 10
秒再执行,在任务中设置 睡眠时间 为 10
秒。这里只验证了 @Scheduled(initialDelay = 2000L, fixedDelay = 10 * 1000L)。
2018-06-25 18:00:53.051 INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService : Schedule executor 1 times with fixed delay
2018-06-25 18:01:13.056 INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService : Schedule executor 2 times with fixed delay
2018-06-25 18:01:33.061 INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService : Schedule executor 3 times with fixed delay
2018-06-25 18:01:53.071 INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService : Schedule executor 4 times with fixed delay
2018-06-25 18:02:13.079 INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService : Schedule executor 5 times with fixed delay
复制代码
上述配置都是基于 单线程 的任务调度,如何引入 多线程 提升 延时任务 的 并发处理 能力?
Spring Boot
提供了一个 SchedulingConfigurer
配置接口。咱们经过 ScheduleConfig
配置文件实现 ScheduleConfiguration
接口,并重写 configureTasks()
方法,向 ScheduledTaskRegistrar
注册一个 ThreadPoolTaskScheduler
任务线程对象便可。
@Configuration
public class ScheduleConfiguration implements SchedulingConfigurer {
private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleConfiguration.class);
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setTaskScheduler(taskScheduler());
}
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(4);
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
taskScheduler.setThreadNamePrefix("schedule");
taskScheduler.setRemoveOnCancelPolicy(true);
taskScheduler.setErrorHandler(t -> LOGGER.error("Error occurs", t));
return taskScheduler;
}
}
复制代码
启动 Spring Boot
引用,上面 SpringTaskService
配置的 4
个定时任务会同时生效。
2018-06-20 20:37:50.746 INFO 8142 --- [ schedule1] i.o.s.sample.spring.SpringTaskService : Schedule executor 1 times at fixed rate
2018-06-20 20:38:00.001 INFO 8142 --- [ schedule3] i.o.s.sample.spring.SpringTaskService : Schedule executor 2 times with
2018-06-20 20:38:00.751 INFO 8142 --- [ schedule1] i.o.s.sample.spring.SpringTaskService : Schedule executor 3 times at fixed rate
2018-06-20 20:38:02.748 INFO 8142 --- [ schedule2] i.o.s.sample.spring.SpringTaskService : Schedule executor 4 times with fixed delay
2018-06-20 20:38:10.005 INFO 8142 --- [ schedule4] i.o.s.sample.spring.SpringTaskService : Schedule executor 5 times with
2018-06-20 20:38:10.747 INFO 8142 --- [ schedule3] i.o.s.sample.spring.SpringTaskService : Schedule executor 6 times at fixed rate
2018-06-20 20:38:20.002 INFO 8142 --- [ schedule2] i.o.s.sample.spring.SpringTaskService : Schedule executor 7 times with
2018-06-20 20:38:20.747 INFO 8142 --- [ schedule4] i.o.s.sample.spring.SpringTaskService : Schedule executor 8 times at fixed rate
复制代码
观察日志,线程名前缀 为 schedule
,能够发现 Spring Task
将 @Scheduled
注解配置的 4
个任务,分发给咱们配置的 ThreadPoolTaskScheduler
中的 4
个线程并发执行。
本文介绍了基于单节点的定时任务调度及实现,包括 JDK
原生的 Timer
和 ScheduledExecutorService
,以及 Spring 3.0
之后自带的基于注解的 Spring Task
任务调度方式。除此以外,重点阐述了基于 固定延时、固定频率 和 cron
表达式 的不一样之处,并对 ScheduledExecutorService
和 Spring Scheduler
的 线程池并发处理 进行了测试。
欢迎关注技术公众号: 零壹技术栈
本账号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。