详解Spring Boot定时任务的几种实现方案

1.概述

在 Spring Boot 中,定时任务的实现方案多种多样,本文主要基于单机模式环境下讲述,所谓单机就是一个Java应用服务,至于集群分布式定时任务之前有总结过,感兴趣的可去公众号自行查看。

关于单机定时任务实现方式有如下几种:

  • Java原生提供的ScheduledExecutorServiceTimer
  • Spring Task提供的 @Scheduled 注解实现定时任务

2.古老定时任务工具Timer

Timer 是比较古老的定时任务工具,不推荐使用了,在 Java 1.3 引入的用于执行定时任务。Timer是通过单线程调度任务的,使用 Timer 类和 TimerTask 类配合实现定时任务。示例如下:

public class TimerTest {
    public static void main(String[] args) {

        TimerTask task1 = new TimerTask() {
            @SneakyThrows
            @Override
            public void run() {
                System.out.println("task1  run:"+ new Date());
                TimeUnit.SECONDS.sleep(6);
            }
        };
        TimerTask task2 = new TimerTask() {
            @Override
            public void run() {
                System.out.println("task2  run:"+ new Date());
            }
        };
        System.out.println("开始执行了。。。" + new Date());
        Timer timer = new Timer();
        //安排指定的任务在指定的时间开始进行重复的固定延迟执行。这里是0秒延时即立即执行,每10秒执行一次
        timer.schedule(task1,0,10000);
        timer.schedule(task2,0,10000);

    }
}

执行结果:

开始执行了。。。Mon Dec 16 10:32:19 CST 2024
task1  run:Mon Dec 16 10:32:19 CST 2024
task2  run:Mon Dec 16 10:32:25 CST 2024

可以看出task1和task2虽然都是同时启动执行任务,但是执行时间相隔6s,正好是task1执行任务睡眠的时间,这有力的说明了Timer是单线程执行任务的,也就是task1执行完了,task2才执行。

public class TimerTest {
    public static void main(String[] args) {

        TimerTask task1 = new TimerTask() {
            @SneakyThrows
            @Override
            public void run() {
                System.out.println("task1  run:"+ new Date());
                TimeUnit.SECONDS.sleep(6);
                throw new RuntimeException("task1 error...");
            }
        };
        TimerTask task2 = new TimerTask() {
            @Override
            public void run() {
                System.out.println("task2  run:"+ new Date());
            }
        };
        System.out.println("开始执行了。。。" + new Date());
        Timer timer = new Timer();
        //安排指定的任务在指定的时间开始进行重复的固定延迟执行。这里是每10秒执行一次
        timer.schedule(task1,0,10000);
        timer.schedule(task2,0,10000);

    }
}

执行结果:

开始执行了。。。Mon Dec 16 10:39:26 CST 2024
task1  run:Mon Dec 16 10:39:26 CST 2024
Exception in thread "Timer-0" java.lang.RuntimeException: task1 error...
  at com.shepherd.basedemo.schedule.TimerTest$1.run(TimerTest.java:25)
  at java.util.TimerThread.mainLoop(Timer.java:555)
  at java.util.TimerThread.run(Timer.java:505)

Process finished with exit code 0

这里我给出了控制台的全部输出结果,由此可见在task1报错了,整个任务就停掉了,既没有执行task2,也没有按照定时需求10s后再次执行,这显然是不行的。其实在阿里巴巴Java开发手册中就有明确规定不再允许使用Timer来实现定时任务。

3.ScheduledExecutorService

ScheduledExecutorService 是 Java 1.5 引入的,是 java.util.concurrent 包的一部分。提供线程池支持,允许多个任务并行执行。是现代化、高效、线程安全的任务调度工具,推荐使用。简单来说就是该类是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。话不多说直接看示例:

  public static void main(String[] args) {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);

        // 任务1:延时任务
  5秒后执行,只执行1次
        scheduler.schedule(() -> System.out.println("task1 run: " + DateUtil.formatDateTime(new Date()) + "         threadName:" + Thread.currentThread().getName()), 5, TimeUnit.SECONDS);

        // 任务2:延迟1秒后执行,每隔2秒执行一次
        scheduler.scheduleAtFixedRate(() -> {
            System.out.println("task2 run: " + DateUtil.formatDateTime(new Date()) + " threadName:"
            + Thread.currentThread().getName());
        }, 1, 2, TimeUnit.SECONDS);

        // 任务3:上一个任务结束后,延迟2秒执行
        scheduler.scheduleWithFixedDelay(() -> {
            System.out.println("task3 run: " + DateUtil.formatDateTime(new Date()) + " threadName:"
                    + Thread.currentThread().getName());
        }, 1, 2, TimeUnit.SECONDS);
    }

执行结果:

task2 run: 2024-12-16 11:40:06 threadName:pool-1-thread-1
task3 run: 2024-12-16 11:40:06 threadName:pool-1-thread-2
task2 run: 2024-12-16 11:40:08 threadName:pool-1-thread-1
task3 run: 2024-12-16 11:40:08 threadName:pool-1-thread-3
task2 run: 2024-12-16 11:40:10 threadName:pool-1-thread-1
task1 run: 2024-12-16 11:40:10 threadName:pool-1-thread-2
task3 run: 2024-12-16 11:40:10 threadName:pool-1-thread-3
task2 run: 2024-12-16 11:40:12 threadName:pool-1-thread-1
task3 run: 2024-12-16 11:40:12 threadName:pool-1-thread-2
task2 run: 2024-12-16 11:40:14 threadName:pool-1-thread-3
task3 run: 2024-12-16 11:40:14 threadName:pool-1-thread-1
task2 run: 2024-12-16 11:40:16 threadName:pool-1-thread-1
task3 run: 2024-12-16 11:40:16 threadName:pool-1-thread-3

从结果上来看ScheduledExecutorService确实是多线程的,同一时间两个任务执行顺序不定且互相独立。使用起来非常简单直接。我们也注意到task1有且仅执行了一次,这不就是妥妥的延时任务吗,需要实现简单延时任务完全可以使用它来搞定,比使用消息队列rabbitMQ, rocketMQ基于死信队列

实现延时任务处理来的快多了。

既然ScheduledExecutorService是当前Java提供的主流定时任务并推荐使用,我们这里就来好好分析下吧,先来来看看其定义如下所示:

public interface ScheduledExecutorService extends ExecutorService {
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

主要方法

  • schedule(Runnable command, long delay, TimeUnit unit):延迟执行任务。
  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):以固定的速率重复执行任务。
  • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):以固定的延迟重复执行任务。

参数含义:这里也scheduleAtFixedRate方法为例

  • Runnable command: 任务体,也就是定时任务执行的核心逻辑
  • long initialDelay: 首次执行的延时时间
  • long period: 任务执行间隔
  • TimeUnit unit: 首次延时执行和周期间隔时间单位

该接口继承了Java并发包线程池工具封装的上次接口ExecutorService,这就意味着ScheduledExecutorService拥有多线程处理任务的能力,这和我们上面的介绍是吻合的。其核心实现类是:

public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService {
        }

可以看到ScheduledThreadPoolExecutor继承了ThreadPoolExecutorThreadPoolExecutor不正是Java并发包实现线程池的核心类吗,不清楚的可以跳转之前总结的文章查看:

关于ScheduledThreadPoolExecutor源码解读,碍于篇幅问题这里就不做过度解读了,有兴趣可以直接去看看源码。

如果需要取消任务,可以使用 Future 对象:

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
ScheduledFuture<?> future = scheduler.schedule(() -> System.out.println("Task executed"), 5, TimeUnit.SECONDS);

// 取消任务
future.cancel(false);
项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构

底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用


4.使用 @Scheduled 注解实现定时任务

4.1 基础入门

上面讲的两个单机定时任务工具都是JDK提供的,接下来我们来看看Spring Scheduler,它是 Spring 框架中提供的一种定时任务实现,基于 Spring 的功能封装,便于集成到 Spring 应用中,可以这么说我们在平时使用Spring Boot开发系统中一定使用过@Scheduled实现定时任务,直接上示例:

@Component
@Slf4j
public class ScheduledTask {
    // 每隔5秒执行一次
    @Scheduled(fixedRate = 5000)
    public void taskWithFixedRate() {
        log.info("Fixed Rate Task: " + DateUtil.formatDateTime(new Date()));
    }

    // 首次延时3s执行,上次任务结束后5秒再执行
    @Scheduled(fixedDelay = 5000, initialDelay = 3000)
    public void taskWithFixedDelay() {
        log.info("Fixed Delay Task: " + DateUtil.formatDateTime(new Date()));
    }

    // 使用 cron 表达式
,10秒执行一次
    @Scheduled(cron = "0/10 * * * * ?")
    public void taskWithCron() {
        log.info("Cron Task: " + DateUtil.formatDateTime(new Date()));
    }
}

在项目启动类上加上注解@EnableScheduling

@SpringBootApplication
@EnableScheduling
public class BaseDemoApplication {

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

参数说明

  1. fixedRate:固定速率执行任务,不考虑任务的执行时间。
  2. fixedDelay:固定延迟执行任务,即上次任务完成后等待指定时间再执行。
  3. cron:使用 Cron 表达式定义任务调度
  1. 规则,支持秒级别精确调度。

启动项目执行结果如下:

[common-demo] [] [2024-12-16 16:44:59.973] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedRate: Fixed Rate Task: 2024-12-16 16:44:59
[common-demo] [] [2024-12-16 16:45:00.005] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithCron: Cron Task: 2024-12-16 16:45:00
[common-demo] [] [2024-12-16 16:45:02.892] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedDelay: Fixed Delay Task: 2024-12-16 16:45:02
[common-demo] [] [2024-12-16 16:45:04.891] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedRate: Fixed Rate Task: 2024-12-16 16:45:04
[common-demo] [] [2024-12-16 16:45:07.901] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedDelay: Fixed Delay Task: 2024-12-16 16:45:07
[common-demo] [] [2024-12-16 16:45:09.890] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedRate: Fixed Rate Task: 2024-12-16 16:45:09
[common-demo] [] [2024-12-16 16:45:10.001] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithCron: Cron Task: 2024-12-16 16:45:10
[common-demo] [] [2024-12-16 16:45:12.904] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedDelay: Fixed Delay Task: 2024-12-16 16:45:12
[common-demo] [] [2024-12-16 16:45:14.890] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedRate: Fixed Rate Task: 2024-12-16 16:45:14
[common-demo] [] [2024-12-16 16:45:17.907] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedDelay: Fixed Delay Task: 2024-12-16 16:45:17
[common-demo] [] [2024-12-16 16:45:19.891] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedRate: Fixed Rate Task: 2024-12-16 16:45:19
[common-demo] [] [2024-12-16 16:45:20.002] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithCron: Cron Task: 2024-12-16 16:45:20
[common-demo] [] [2024-12-16 16:45:22.911] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedDelay: Fixed Delay Task: 2024-12-16 16:45:22

从执行结果输出可以看出三个任务是由同一个线程串行执行的,当定时任务增多,如果一个任务卡死,会导致其他任务也无法执行。同时也注意到了Fixed Delay Task 比Fixed Rate Task晚了3s执行,因为我们配置了initialDelay = 3000。关于多个任务在同一个线程中串行执行我们再来看看一个示例:

@Component
@Slf4j
public class ScheduledTask {
    // 使用 cron 表达式, 每10秒执行一次
    @SneakyThrows
    @Scheduled(cron = "0/10 * * * * ?")
    public void taskWithCron() {
        log.info("task1: " + DateUtil.formatDateTime(new Date()));
        // 模拟task1执行需要耗费8s
        TimeUnit.SECONDS.sleep(8);
    }
}

@Component
@Slf4j
public class ScheduledTask2 {
    // 使用 cron 表达式, 每10秒执行一次
    @Scheduled(cron = "0/10 * * * * ?")
    public void taskWithCron() {
        log.info("task2: " + DateUtil.formatDateTime(new Date()));
    }
}

这里我们定义了两个任务类分别执行定时任务,并且让task1 睡眠了8s模拟任务逻辑耗时,看看task2执行时间:

[common-demo] [] [2024-12-16 17:00:30.126] [INFO] [scheduling-1@42202] com.shepherd.basedemo.schedule.ScheduledTask taskWithCron: task1: 2024-12-16 17:00:30
[common-demo] [] [2024-12-16 17:00:38.138] [INFO] [scheduling-1@42202] com.shepherd.basedemo.schedule.ScheduledTask2 taskWithCron: task2: 2024-12-16 17:00:38

很明显task1和task2还是同一个线程串行执行,所以task2是等着task1执行完之后才执行的。

4.2 结合@Async 实现异步定时任务

Spring 的 @Async 注解可以实现异步任务调度,结合 @Scheduled 可以异步执行定时任务。

使用@Async 的时候,一般都会自定义线程池,因为@Async的默认线程池为SimpleAsyncTaskExecutor,不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。

@Configuration
public class AsyncConfig {

    /**
     * 初始化一个线程池,放入spring beanFactory
     * @return
     */
    @Bean(name = "asyncExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(200);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("asyncExecutor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}

需要在启动类或配置类中开启异步任务:

@SpringBootApplication
@EnableAsync // 开启异步任务
@EnableScheduling // 开启定时任务
public class BaseDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(AsyncScheduledTaskApplication.class, args);
    }
}
展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java