为了账号安全,请及时绑定邮箱和手机立即绑定

实战Spring Boot 2.0系列(六) - 单机定时任务的几种实现

前言

定时任务 一般会存在 中大型企业级 项目中为了减少 服务器数据库 的压力往往会以 定时任务 的方式去完成某些业务逻辑。

常见的就是 金融服务系统 推送回调一般支付系统订单在没有收到成功的回调返回内容时会 持续性的回调这种回调一般都是 定时任务 来完成。

还有就是 报表的生成我们一般会在客户 访问量小 时完成这个操作也可以采用 定时任务 来完成。

正文

定时任务的几种方式

Timer

这是 Java 自带的 java.util.Timer 类这个类允许调度一个名为 java.util.TimerTask 任务。使用这种方式可以让你的程序按照某一个 频度 执行但不能在 指定时间 运行。现在一般用的较少。

ScheduledExecutorService

JDK 自带的一个类是基于 线程池 设计的定时任务类每个 调度任务 都会分配到 线程池 中的一个 线程 去执行。也就是说任务是 并发执行互不影响的。

Spring Task

Spring 3.0 以后自带的 Task支持 多线程 调度可以将它看成一个 轻量级Quartz而且使用起来比 Quartz 简单许多但是适用于 单节点定时任务调度

Quartz

这是一个 功能比较强大 的的调度器可以让你的程序在指定时间执行也可以按照某一个频度执行配置起来 稍显复杂Quartz 功能强大可以结合 数据库持久化进行 分布式任务延时调度

Cron表达式简介

Cron 表达式是一个字符串字符串以 56空格 隔开分为 67每一个域代表一个含义Cron 有如下两种语法格式

  1. Seconds Minutes Hours DayofMonth Month DayofWeek Year
  2. Seconds Minutes Hours DayofMonth Month DayofWeek

每个域对应的含义、域值范围和特殊表示符从左到右依次如下

字段 允许值 允许的特殊字符
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” 在星期域里表示 “星期一、星期三、星期五”。

  • "?"字符: 只在日期域和星期域中使用。它被用来指定"非明确的值"。当你需要通过在这两个域中的一个来指定一些东西的时候它是有用的。看下面的例子你就会明白。

  • "L"字符: 指定在月或者星期中的某天最后一天。即 “Last” 的缩写。但是在星期和月中 “” 表示不同的意思如在月子段中 “L” 指月份的最后一天 - 1月31日2月28日。

    • 如果在星期字段中则简单的表示为 “7” 或者 “SAT” 字符。
    • 如果在星期字段中在某个 value 值得后面则表示 “某月的最后一个星期value”如 “6L” 表示某月的最后一个星期五。
  • "W"字符: 只能用在月份字段中该字段指定了离指定日期最近的那个星期日。

  • "#"字符: 只能用在星期字段该字段指定了第几个星期 value 在某月中

每一个元素都可以显式地规定一个值如 6一个区间如 9-12一个列表如 91113或一个通配符如 *“月份中的日期”“星期中的日期” 这两个元素是 互斥的因此应该通过设置一个 问号?来表明你不想设置的那个字段。下表显示了一些 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/28 * * * 晚上11点到早上8点之间每两个小时早上八点
0 11 4 * 1-3 每个月的4号和每个礼拜的礼拜一到礼拜三的早上11点
0 4 1 1 * 1月1日早上4点

环境准备

配置gradle依赖

利用 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);
    }
}

配置Timer任务

这个 API 目前在项目中很少用直接给出示例代码。具体的介绍可以查看 APITimer 的内部只有 一个线程如果有 多个任务 的话就会 顺序执行这样任务的 延迟时间循环时间 就会出现问题。

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;
    }
}

上面的代码定义了一个 TimerTaskTimerTask 中累加 执行次数并通过 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延时执行 的线程池对于 多线程 环境下的 定时任务推荐用 ScheduledExecutorService 代替 Timer 定时器。

创建一个线程数量为 4任务线程池同一时刻并向它提交 4 个定时任务用于测试延时任务的 并发处理。执行 ScheduledExecutorServicescheduleWithFixedDelay() 方法设置任务线程池的 初始任务延迟时间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 个定时任务用于测试延时任务的 并发处理。每个任务分别执行 ScheduledExecutorServicescheduleAtFixedRate() 方法设置任务线程池的 初始任务延迟时间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 Task任务

Spring 提供了 @Scheduled 注解来实现 定时任务@Scheduled 参数可以接受 两种 定时的设置一种是我们常用的 格林时间表达式 cron = "*/10 * * * * *"另一种是 fixedRate = 10 * 1000L两种都表示每隔 10 秒执行一次目标任务。

参数说明

  • @Scheduled(fixedRate = 10 * 1000L)上一次 开始执行时间点 之后 10 秒再执行。
@Scheduled(fixedRate = 10 * 1000L)
public void scheduleAtFixedRate() throws Exception {
    long count = counter.incrementAndGet();
    LOGGER.info("Schedule executor {} times at fixed rate", count);
}
  • @Scheduled(fixedDelay = 10 * 1000L)上一次 执行完毕时间点 之后 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);
}
  • @Scheduled(initialDelay = 2000L, fixedRate = 10 * 1000L)第一次延迟 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);
}
  • @Scheduled(cron = “0/10 * * * * *”)根据 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 原生的 TimerScheduledExecutorService以及 Spring 3.0 以后自带的基于注解的 Spring Task 任务调度方式。除此之外重点阐述了基于 固定延时固定频率cron 表达式 的不同之处并对 ScheduledExecutorServiceSpring Scheduler线程池并发处理 进行了测试。


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消