详解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并发包实现线程池的核心类吗,不清楚的可以跳转之前总结的文章查看:

你知道怎么合理设置线程池参数吗?

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

Github地址:https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨qun:Shepherd_126

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 表达式定义任务调度规则,支持秒级别精确调度。

启动项目执行结果如下:

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

在上面的定时任务分别加上注解@Async

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

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

执行结果如下所示:

[common-demo] [] [2024-12-16 17:17:40.136] [INFO] [asyncExecutor-3@47804] com.shepherd.basedemo.schedule.ScheduledTask taskWithCron: task1: 2024-12-16 17:17:40
[common-demo] [] [2024-12-16 17:17:40.136] [INFO] [asyncExecutor-2@47804] com.shepherd.basedemo.schedule.ScheduledTask2 taskWithCron: task2: 2024-12-16 17:17:40
[common-demo] [] [2024-12-16 17:17:50.012] [INFO] [asyncExecutor-7@47804] com.shepherd.basedemo.schedule.ScheduledTask2 taskWithCron: task2: 2024-12-16 17:17:50
[common-demo] [] [2024-12-16 17:17:50.013] [INFO] [asyncExecutor-4@47804] com.shepherd.basedemo.schedule.ScheduledTask taskWithCron: task1: 2024-12-16 17:17:50

可以看出两个任务是由不同线程并行执行的。

5.总结

尽量避免使用 Timer,因为它的单线程实现和异常处理问题容易引发严重后果,在现代 Java 开发中,尽量使用 ScheduledExecutorService 替代 Timer,但是要注意配置合理的线程池大小,避免任务积压或线程资源浪费,使用 try-catch 捕获任务中的异常,防止任务本身问题导致日志混乱。对于复杂任务调度,可以使用 Spring Scheduler

ScheduledExecutorServiceSpring Scheduler两者对比:

特性ScheduledExecutorServiceSpring Scheduler
引入版本Java 1.5Spring Framework
配置方式编程式(手动管理线程池和任务调度)注解式配置,简单易用
线程池支持支持线程池,需手动管理内置线程池,Spring 自动管理
Cron 表达式支持不直接支持,需手动解析或结合第三方工具支持 Cron 表达式,规则配置更灵活
任务隔离多线程任务隔离,异常任务不会影响其他任务任务隔离,异常任务不影响其他任务
复杂调度场景灵活,适用于复杂任务管理和生命周期控制不适合需要动态调整线程池或大规模任务调度的场景
分布式支持可结合其他工具实现分布式任务调度不支持分布式调度,需结合 Quartz 等工具扩展
推荐场景高性能、多任务并发调度、复杂调度需求简单任务调度,Spring 应用中的轻量任务管理

最后的最后,想说一下最近年底的写文章情况,一个是年底比较忙另一个是写文章热情感觉低迷~,所以最近输出不够频繁了。。。这篇文章相对来说比较基础不是之前我的风格,但其实这里只是铺垫下,我会紧接着总结下定时任务在实战中出现问题和解决方案,敬请期待,马上立刻就会安排上了,还有你们有什么感兴趣的,或者说有什么需要交流沟通的,可以在留言区讨论哈。如果本文对你有帮助的话,麻烦给个一键三连(点赞、在看、转发分享)支持一下,感谢Thanks♪(・ω・)ノ

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值