应用场景

有贷款的同学每个月都会收到催还贷款的提醒短信,每天上班、上课前钉钉的打卡提醒,等等。类似这种定时重复的功能,我们就可以使用任务调度来实现。

任务调度框架

调度框架说明
TimerJDK自带类java.util.Timer,最简单的一种实现任务调度的方法。Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。
ScheduledExecutor鉴于 Timer 的缺陷,Java 5 推出了基于线程池设计的 ScheduledExecutor。其设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。
Spring Boot的@Scheduled注解使用简单,几乎不用编码。满足常见的任务调度需求。很实用。但不适用于分布式集群环境
quartz应该是目前使用最多的任务调度框架了,功能丰富,支持分布式集群。但不支持任务分片等更高级的功能
ElasticJobElasticJob是一个分布式调度解决方案,由两个独立的项目组成,ElasticJob- lite和ElasticJob- cloud。ElasticJob-Lite是一个轻量级的,分散的解决方案,提供分布式的任务分片服务;ElasticJob-Cloud利用Mesos来管理和隔离资源。它为每个项目使用统一的作业API。开发人员只需要一次代码,并且可以随意部署。
XXL-JOB国人开源的分布式任务调度框架,官方中文文档,方便学习。也支持任务分片等。大众点评等一些互联网大厂也在使用。
其它还有LTS等新兴的任务调度框架,个人觉得,使用的时候选择合适的框架就行了,如果你的项目用不了那么多的功能,或者就是个单应用、或者根本不需要分片。因为功能越强大,那么使用起来,实现起来就没有那么容易,这个要看你的项目团队的人员构成和项目的开发成本是否支持你选用更牛逼的架构。否则你的项目很有可能陷入被动调整,或者无法按时交付的风险。

Spring Boot的@Scheduled注解

在单应用下,我们可以使用Spring Boot的@Scheduled注解来实现简单的任务调度。

首先需要使用@EnableScheduling注解开启任务调度

@RestController
@SpringBootApplication(scanBasePackages = "com.yyoo")
// 开启任务调度
@EnableScheduling
public class Appliction {

    public static void main(String[] args) {
        SpringApplication.run(Appliction.class, args);
    }

}

使用@Scheduled注解任务执行的方法

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class TaskDemo1 {

    @Scheduled(cron = "1 * * * * ?")
    public void doTask1(){
        // 实际上就是每分钟一次
        System.out.println("每分钟的第1秒执行一次");
    }

    @Scheduled(cron = "1/1 * * * * ?")
    public void doTask2(){
        // 分子的1表示从第1秒开始执行,分母表示每隔1秒执行一次。
        // 但分子我们常常写为*,比如每10秒执行一次*/10
        // 如果需要每隔5秒执行一次,就是除以5
        System.out.println("每秒执行一次");
    }

}

cron表达式

任务调度不可避开的要编写cron表达式,各个调度框架、包括linux服务器都有cron表达式来表示任务调度的时间、频率等。(注:各个框架或者linux服务器支持的cron表达式可能会有些许的不同。)
以下按照cron表达式从左至右的顺序

0~590~590~231~311~121~7 1是周日,7是周六1970~2099

天和周不能同时表达具体的值,因为1号不一定是周1,所以我们的表达式最后那个周,用?。同理,如果我们要表达具体的周几,那么天这里就只能是?。

年通常我们都不怎么使用,所以我们的示例中只有6位,而没有最后一位年。

cron表达式中的符号

符号说明
*表示任意时刻
?在天或者周上使用
-表示范围,如:在天上使用1-15,表示1号到15。
/表示间隔,指定时间的间隔频率,例如“0-23/2”表示每两小时执行一次。同时正斜线可以和星号一起使用,例如*/10,如果用在minute字段,表示每十分钟执行一次
,表示枚举,如:在分钟上使用10,30,45,55表示在每个小时的10分钟、30分钟、45分钟、55分钟时执行
L表示最后的天 或者 周,如在天上使用,L3表示倒数第三天
#表示第几个星期几,在周上使用,如:7#3 表示第三个周六

@Scheduled的其他用法

//    @Scheduled(fixedDelay = 5000)
    // 时间默认单位是毫秒,我们可以通过timeUnit属性修改
    @Scheduled(fixedDelay = 5,timeUnit = TimeUnit.SECONDS)
    public void doTask3() throws InterruptedException {
        Thread.sleep(10000);// 休眠10秒来验证(意味着至少要等15秒之后才会再次执行)
        // 上次调用结束后5秒执行
        System.out.println("上次调用结束后5秒执行"+Thread.currentThread().getName());
    }

    @Scheduled(fixedRate = 5,timeUnit = TimeUnit.SECONDS)
    public void doTask4() throws InterruptedException {
        // 每隔5秒执行,无论上次执行是否成功,是否结束
        System.out.println("每隔5秒执行"+Thread.currentThread().getName());
        Thread.sleep(10000);
    }

细心一点的同学运行程序后会发现,我们目前定义了4个任务,貌似执行起来已经不太对劲了。比如每秒执行的任务很久都不执行了。why?

如果我们每个任务方法中都打线程名称后你会发现,打印出的值都是:scheduling-1。说明我们的所有任务都是同一个线程执行的。导致了我们的任务串行化了。此时我们可以通过ThreadPoolTaskScheduler 或 @Async注解来解决。

配置ThreadPoolTaskScheduler来解决串行化

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@Configuration
public class ScheduleConfig {

    @Bean
    public TaskScheduler taskScheduler(){
        ThreadPoolTaskScheduler tpts = new ThreadPoolTaskScheduler();

        tpts.setPoolSize(5);// 线程池数量
        tpts.setThreadNamePrefix("my-Task-");// 线程名称前缀

        return tpts;
    }

}

@Async异步执行任务调度

与@Scheduled一样,@Async也需要一个注解来开启。在Application类上添加@EnableAsync注解来开启@Async注解支持。最后我们的示例代码如下:

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class TaskDemo1 {

    @Scheduled(cron = "1 * * * * ?")
    @Async
    public void doTask1(){
        // 实际上就是每分钟一次
        System.out.println("每分钟的第1秒执行一次"+Thread.currentThread().getName());
    }

    @Scheduled(cron = "1/1 * * * * ?")
    @Async
    public void doTask2(){
        // 分子的1表示从第1秒开始执行,分母表示每隔1秒执行一次。
        // 但分子我们常常写为*,比如每10秒执行一次*/10
        // 如果需要每隔5秒执行一次,就是除以5
        System.out.println("每秒执行一次"+Thread.currentThread().getName());
    }

//    @Scheduled(fixedDelay = 5000)
    // 时间默认单位是毫秒,我们可以通过timeUnit属性修改
    @Scheduled(fixedDelay = 5,timeUnit = TimeUnit.SECONDS)
    @Async
    public void doTask3() throws InterruptedException {
        Thread.sleep(10000);// 休眠10秒来验证(意味着至少要等15秒之后才会再次执行)
        // 上次调用结束后5秒执行
        System.out.println("上次调用结束后5秒执行"+Thread.currentThread().getName());
    }

    @Scheduled(fixedRate = 5,timeUnit = TimeUnit.SECONDS)
    @Async
    public void doTask4() throws InterruptedException {
        // 每隔5秒执行,无论上次执行是否成功,是否结束
        System.out.println("每隔5秒执行"+Thread.currentThread().getName());
        Thread.sleep(10000);
    }

}

执行后打印结果正确了,而且线程名字也不一样了。

@Async注释的方法还可以定义异步Future返回

@Async
Future<String> returnSomething(int i) {
    // this will be run asynchronously
}

使用@Async来异步调用之后,我们之前配置的ThreadPoolTaskScheduler就没有效果了。因为你会看到有第6、第7甚至第8个线程出现。使用@Async来处理异步任务调度,spring自动控制线程的数量,如果需要自己控制,就自定义配置ThreadPoolTaskScheduler把。

本人建议:不要直接使用@Async注解来实现异步任务调度,应@Async底层的实现是一个无界队列,当你的定时任务过多单个任务执行时间过长,甚至超过你的执行间隔时间时,会导致任务队列过大过多(任务积压),导致内存溢出等问题。关于这些相关的问题,我们后续可以做一个单独的文章来进行相关说明。

上一篇:Spring Boot logback日志
下一篇:待续

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐