从零开始 Spring Boot 40:定时任务

news2024/11/28 20:37:47

从零开始 Spring Boot 40:定时任务

spring boot

图源:简书 (jianshu.com)

定时任务是一种很常见的需求,比如我们可能需要应用定期去执行一些清理工作,再比如可能需要定期检查一些外部服务的可用性等。

fixedDelay

要在 Spring 中开启定时任务相关功能,需要在任意的配置类上添加上@EnableScheduling

@Configuration
@EnableScheduling
public class WebConfig {
}

之后就可以在 Spring Bean 中定义一个定时任务对应的方法:

@Component
public class MySchedule {
    @Scheduled(fixedDelay = 1000)
    public void sayHello() {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("hello, now is %s".formatted(now));
    }
}

在这个示例中,@Scheduled(fixedDelay = 1000)表示所标记的方法将每隔1000毫秒(1秒)执行一次。fixedDelay中的值代表下一次任务将在前一次任务执行完毕后的若干毫秒后执行。

所以这个示例执行后能看到类似下面的输出:

hello, now is 2023-06-14T15:27:21.026108300
hello, now is 2023-06-14T15:27:22.034320100
hello, now is 2023-06-14T15:27:23.038852900
hello, now is 2023-06-14T15:27:24.054180300
hello, now is 2023-06-14T15:27:25.065741600

要注意的是,定时任务对应的方法返回值必须是void,且不能包含任何参数。

fixedRate

除了上边介绍的方式外,还可以指定“每间隔若干时间后执行定时任务”这种模式:

@Component
public class MySchedule {
    @Scheduled(fixedRate = 1000)
    public void sayHello() {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("hello, now is %s".formatted(now));
    }
}

@ScheduledfixedRate属性可以设置每次任务执行间隔的毫米数。

fixedDelay vs fixedRate

fixedDelayfixedRate是有区别的,虽然上面的两个例子输出看起来一样,但这是因为任务执行的时间太短造成的,我们将任务执行的时间放长:

@Component
public class MySchedule {
    @Scheduled(fixedDelay = 1000)
    public void sayHello() throws InterruptedException {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("hello, now is %s".formatted(now));
        Thread.sleep(2000);
    }
}

输出:

hello, now is 2023-06-14T15:34:22.876346500
hello, now is 2023-06-14T15:34:25.895804500
hello, now is 2023-06-14T15:34:28.908301500
hello, now is 2023-06-14T15:34:31.937000600
hello, now is 2023-06-14T15:34:34.948180300
@Component
public class MySchedule {
    @Scheduled(fixedRate = 1000)
    public void sayHello() throws InterruptedException {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("hello, now is %s".formatted(now));
        Thread.sleep(2000);
    }
}

输出:

hello, now is 2023-06-14T15:35:22.794709900
hello, now is 2023-06-14T15:35:24.798962500
hello, now is 2023-06-14T15:35:26.806299700
hello, now is 2023-06-14T15:35:28.811730
hello, now is 2023-06-14T15:35:30.817310200

可以看到,使用fixedDelay时任务每隔3秒执行一次,使用fixedRate时任务每隔2秒执行一次。

这是因为任务本身的执行时长是2秒(由Thread.sleep决定),而如果定时任务设置为fixedDelay = 1000,那么下一次任务就会在上一次任务结束的1000毫秒后执行,所以两次任务之间的间隔是3秒。

如果定时任务设置为fixedRate = 1000,这本来应该意味着定时任务时间间隔是1000毫秒,但这个示例中,任务本身的时长(2秒)超过了这里指定的任务间隔时长(1秒),因此调度器将会在上一次任务执行完毕后立即执行下次任务,因此此时任务之间的实际执行间隔就是任务本身的时长(2秒)。

并发的 fixedRate 任务

之所以fixedRate表现如此,是因为默认的定时任务调度器是一个单线程,因此所有的定时任务只能顺序执行。换言之,如果我们想要让设置为fixedRate的定时任务并发,可以:

@Configuration
@EnableScheduling
@EnableAsync
public class WebConfig {
}

@Component
public class MySchedule {
    @Async
    @Scheduled(fixedRate = 1000)
    public void sayHello() throws InterruptedException {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("hello, now is %s".formatted(now));
        Thread.sleep(2000);
    }
}

输出:

hello, now is 2023-06-14T15:45:44.853280700
hello, now is 2023-06-14T15:45:45.851481600
hello, now is 2023-06-14T15:45:46.863900700
hello, now is 2023-06-14T15:45:47.858285100
hello, now is 2023-06-14T15:45:48.864664300
hello, now is 2023-06-14T15:45:49.857035800

可以看到,无论前一个任务有没有执行完毕,下一个任务也会在固定时间(这里是1秒)后被执行。

fixedDelay也可以使用@Async让其并发执行,但这样做不符合fixedDelay的意义,会让其“变成”fixedRate,因此应该避免这么做。

initialDelay

可以让定时任务延迟执行,比如:

@Component
public class MySchedule {
    @Scheduled(fixedDelay = 1000, initialDelay = 2000)
    public void sayHello() throws InterruptedException {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("hello, now is %s".formatted(now));
        Thread.sleep(2000);
    }
}

示例中的定时任务第一次执行时将延迟2秒后执行,之后的每次任务将遵循fixedDelay设置,即后一次任务在前一次任务执行完毕后间隔1秒执行。

cron 表达式

除了设置时间间隔,还可以使用 cron 表达式让定时任务在特定时间执行,比如:

@Component
public class MySchedule {
    @Scheduled(cron = "0 */5 * * * *")
    public void sayHello() throws InterruptedException {
		// ...
    }
}

输出:

hello, now is 2023-06-14T16:00:00.007956400
hello, now is 2023-06-14T16:05:00.009349900

示例中的定时任务将在“每个小时的0分钟、5分钟、10分钟…”被执行。

*/5表示可以被5整除的分钟。

这里的 cron 表达式和 Linux 中的写法是类似的,区别是 Linux 的格式是"分钟 小时 日 月 周",而这里是“秒 分钟 小时 日 月 周”,所以这里多了一个秒钟的设置项。

关于 Linux 中的 cron,可以参考Linux 之旅 14:任务计划(crontab) - 红茶的个人站点 (icexmoon.cn)。

外部配置

可以通过外部配置来设置定时任务的执行时间,比如:

@Component
public class MySchedule {
    @Scheduled(cron = "${my.schedule.cron.expression}")
    public void sayHello() throws InterruptedException {
		// ...
    }
}

对应的配置文件:

my.schedule.cron.expression=0 */5 * * * *

动态设置延迟

可以通过编程的方式动态控制定时任务的执行延迟:

@Component
public class DelayService {
    private int delaySeconds = 1;

    @Synchronized
    public int getDelaySeconds() {
        int delaySeconds = this.delaySeconds;
        if (this.delaySeconds <= 1) {
            this.delaySeconds++;
        } else {
            this.delaySeconds--;
        }
        return delaySeconds;
    }
}

@Configuration
@Log4j2
public class MyScheduleConfig implements SchedulingConfigurer {
    @Autowired
    private DelayService delayService;

    @Bean
    public Executor executor() {
        return Executors.newSingleThreadScheduledExecutor();
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(executor());
        taskRegistrar.addTriggerTask(() -> {
            LocalDateTime now = LocalDateTime.now();
            System.out.println("hello, now is %s".formatted(now));
        }, triggerContext -> {
            Instant instant = triggerContext.lastCompletion();
            if (instant == null) {
                instant = new Date().toInstant();
            }
            int delay = delayService.getDelaySeconds();
            instant = instant.plus(delay, ChronoUnit.SECONDS);
            return instant;
        });
    }
}

输出:

hello, now is 2023-06-14T16:41:25.525521800
hello, now is 2023-06-14T16:41:27.534739700
hello, now is 2023-06-14T16:41:28.544731100
hello, now is 2023-06-14T16:41:30.554797300
hello, now is 2023-06-14T16:41:31.564520600

DelayService的作用是返回一个时间间隔,返回的时间间隔会随着调用在1和2之间转换。

考虑到可能的多线程调用,这里的DelayService是线程安全的,具体通过 Lombok 的@Synchronized注解保证这一点,关于 Lombok 的@Synchronized注解,可以阅读我的这篇文章。

为了能够实现动态调整定时任务的触发,我们需要让配置类实现SchedulingConfigurer接口,这样就可以在configureTasks方法中通过ScheduledTaskRegistrar.addTriggerTask方法添加一个定时任务(Runnable)和其对应的触发器(Trigger),这个触发器需要实现的方法最终会返回一个时间点(Instant),一起添加的定时任务就会在该时间点被执行。

实际上Trigger包含两个可以覆盖的方法,其中一个返回Date类型,另一个返回Instant类型,如果你返回的是后者,实际上Trigger自己会转换成Date(通过default方法)。

可以看到,最终的效果就是这个定时任务的时间间隔会在1秒和2秒间来回跳转。

并发

前面我们说过,默认情况下执行定时任务的是一个单线程的任务调度器,每一个定时任务都需要等待前边的定时任务执行完毕后才能开始执行。

比如下面这个示例:

@Component
public class MySchedule {
    @Scheduled(fixedRate = 1000)
    public void sayHello() throws InterruptedException {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("hello, now is %s".formatted(now));
        Thread.sleep(2000);
    }

    @Scheduled(fixedRate = 1000)
    public void sayHello2() throws InterruptedException {
        LocalDateTime now = LocalDateTime.now();
        System.out.println("hello2, now is %s".formatted(now));
        Thread.sleep(2000);
    }
}

输出:

hello, now is 2023-06-14T17:06:12.414944600
hello2, now is 2023-06-14T17:06:14.416957900
hello, now is 2023-06-14T17:06:16.423
hello2, now is 2023-06-14T17:06:18.433155400
hello, now is 2023-06-14T17:06:20.442051500
hello2, now is 2023-06-14T17:06:22.443233500

可以看到,第一个任务sayHello被执行后,sayHello2并不会被立即执行,必须要等到第一个任务执行完毕(2秒后)才开始执行。而sayHello的第二次执行也被推迟到sayHello2的第一次执行完毕后才能开始,此时已经离sayHello第一次执行过去了4秒。

因此,如果我们有多个定时任务,且这些定时任务可以并行执行,那么就可以考虑进行优化。

可以通过自定义任务调度器来改变这一行为:

@Configuration
@EnableScheduling
@EnableAsync
public class WebConfig {
    @Bean
    public TaskScheduler taskScheduler(){
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(5);
        threadPoolTaskScheduler.setThreadNamePrefix("ThreadPoolTaskScheduler");
        return threadPoolTaskScheduler;
    }
}

为此我们需要创建一个TaskScheduler类型的 bean,在示例中使用了ThreadPoolTaskScheduler这个实现类,并通过ThreadPoolTaskScheduler.setPoolSize方法指定了固定的线程池大小(实际使用中应当结合现实情况设置)。此外,还通过ThreadPoolTaskScheduler.setThreadNamePrefix设置了相关线程名称的前缀。

关于TaskScheduler的更多说明,见这篇文章。

现在的输出:

hello, now is 2023-06-14T17:13:34.312232900
hello2, now is 2023-06-14T17:13:36.305893100
hello, now is 2023-06-14T17:13:36.321635900
hello2, now is 2023-06-14T17:13:38.318501500
hello, now is 2023-06-14T17:13:38.333777300
hello2, now is 2023-06-14T17:13:40.333169

可以看到,sayHellosayHello2两个任务几乎同时启动,因为它们使用了ThreadPoolTaskScheduler中两个不同的线程进行执行,所以互相并不影响。

在 Spring Boot 中,实际上并不需要自行定义TaskScheduler类型的 bean,可以通过修改配置的方式设置默认TaskScheduler的线程池大小和线程名称前缀:

spring.task.scheduling.thread-name-prefix=ThreadPoolTaskScheduler
spring.task.scheduling.pool.size=5

效果是相同的。

The End,谢谢阅读。

本文所有的示例代码可以从这里获取。

参考资料

  • A Guide to the Spring Task Scheduler | Baeldung
  • The @Scheduled Annotation in Spring | Baeldung
  • 任务执行和调度 (springdoc.cn)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/646535.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度学习笔记之Transformer(一)注意力机制基本介绍

深度学习笔记之Transformer——注意力机制基本介绍 引言回顾&#xff1a; Seq2seq \text{Seq2seq} Seq2seq模型中的注意力机制注意力机制的简单描述注意力机制的机器学习范例&#xff1a; Nadaraya-Watson \text{Nadaraya-Watson} Nadaraya-Watson核回归 Nadaraya-Watson \text…

编程必备:JAVA多线程详解

目录 前言 1.入门多线程 1.1. 线程、进程、多线程、线程池 1.2.并发、串行、并行 1.3. 线程的实现方式 1.3.1. 继承 Thread 类 1.3.2. 实现 Runnable 接口 1.3.3. 使用 Callable 和 Future 1.3.4. 使用线程池 1.4.线程的状态 1.5. 线程常用方法 1.5.1 sleep() 1.4…

验证码识别系统Python,基于CNN卷积神经网络算法

一、介绍 验证码识别系统&#xff0c;使用Python作为主要开发语言&#xff0c;基于深度学习TensorFlow框架&#xff0c;搭建卷积神经网络算法。并通过对数据集进行训练&#xff0c;最后得到一个识别精度较高的模型。并基于Django框架&#xff0c;开发网页端操作平台&#xff0…

基于Java网上花店系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

Prompt 范式产业实践分享!基于飞桨 UIE-X 和 Intel OpenVINO 实现跨模态文档信息抽取

近期 Prompt 范式备受关注&#xff0c;实际上&#xff0c;其思想在产业界已经有了一些成功的应用案例。中科院软件所和百度共同提出了大一统诸多任务的通用信息抽取技术 UIE&#xff08;Universal Information Extraction&#xff09;。截至目前&#xff0c;UIE 系列模型已发布…

【JavaEE】网络原理——传输层协议:UDP和TCP

目录 1、简单了解应用层协议 2、传输层UDP协议 3、传输层TCP协议 3.1、TCP报文介绍 3.2、TCP实现可靠传输的核心机制 3.2.1、确认应答 3.2.2、超时重传 3.3、连接管理 &#xff08;三次挥手&#xff0c;四次握手&#xff09; 3.3.1、建立连接&#xff08;三次握手&a…

Java-API简析_java.lang.Enum<E extends Enum<E>>类(基于 Latest JDK)(浅析源码)

【版权声明】未经博主同意&#xff0c;谢绝转载&#xff01;&#xff08;请尊重原创&#xff0c;博主保留追究权&#xff09; https://blog.csdn.net/m0_69908381/article/details/131212897 出自【进步*于辰的博客】 其实我的【Java-API】专栏内的博文对大家来说意义是不大的。…

接口自动化测试丨如何实现多套环境的自动化测试?

在敏捷迭代的项目中&#xff0c;通常会将后台服务部署到多套测试环境。那么在进行接口自动化测试时&#xff0c;则需要将服务器的域名进行配置。使用一套接口测试脚本&#xff0c;通过切换域名地址配置&#xff0c;实现多套环境的自动化测试。 实战练习 分别准备两套测试环境…

Redis的单线程模型和标准Reactor线程模型的关系

文章目录 Redis到底是不是单线程&#xff1f;标准Reactor线程模型单reactor单线程单reactor多线程多reactor多线程 redis6.0 之前的单线程模型redis6.0 之后的单线程模型为什么redis最初选择的单线程网络模型&#xff1f;为什么redis6.0 io读写要用多线程&#xff1f; Redis 6.…

索尼RSV视频修复方法论视频文件修复时样本文件的三同

索尼RSV类的文件修复案例有很多&#xff0c;程序操作也很简单没什么可说的&#xff0c;这次这个索尼ILCE-7SM3的案例就是为了让大家更好的认识视频修复中我称之为“三同“的重要性&#xff0c;想要恢复的效果好必须要把准备工作做到位。 故障文件:45.1G RSV文件 故障现象: 索…

软件渗透测试是什么?软件产品哪种情况下需要做渗透测试?

随着互联网的普及&#xff0c;软件的开发方越来越多&#xff0c;但是随之而来的也是信息安全方面的问题。在软件开发过程中&#xff0c;安全问题一定要被重视&#xff0c;因为漏洞和安全问题一旦被黑客利用&#xff0c;会给公司和用户带来巨大的损失。为了避免这种情况的发生&a…

语音工牌在运营商智慧装维场景,有何应用价值?

客户精细化运营时代&#xff0c;如何做好客户服务体验&#xff0c;提升品牌美誉度和好感度&#xff0c;是众多企业开始思考的问题。 在运营商行业&#xff0c;上门装维和营业厅服务场景是企业与客户直接互动最多的地方。这个过程的服务质量直接影响到客户成交率、客户投诉率和…

软件测试金融银行项目如何测?从业务到测试实战,超细总结整理...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 金融行业的业务特…

chatgpt赋能python:Python怎么打备注?让你的代码更加清晰易懂

Python怎么打备注&#xff1f;让你的代码更加清晰易懂 Python是一种流行的编程语言&#xff0c;可以用来构建不同类型的应用程序&#xff0c;从网站到数据分析。无论您是初学者还是经验丰富的开发人员&#xff0c;写清晰&#xff0c;易于理解的代码都是非常重要的&#xff0c;…

Linux之ACL权限

目录 Linux之ACL权限 场景 设定ACL权限 ACL权限管理命令 参数及作用 给用户和用户组添加ACL权限 案例 创建 目录 /project 的所有者和所属组其他人权限设定为 770 创建旁听用户pt,并赋予ACL权限rx 查看目录/project的ACL权限 验证pt 用户对于 /project 目录没有写权…

el-element-admin实现双路由菜单

需求&#xff1a; 1、输入用户名登录企业级菜单 2、点击企业级菜单中的首页&#xff0c;右边显示项目列表&#xff0c;点击某一行跳转到项目级菜单 注意&#xff1a; 企业级菜单和项目级菜单&#xff0c;后端分别给接口 具体实施&#xff1a; 1、点击面包靴首页的时候设置标记…

第14届蓝桥杯国赛真题剖析-2023年5月28日Scratch编程初中级组

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第149讲。 第14届蓝桥杯Scratch国赛真题&#xff0c;这是2023年5月28日上午举办的全国总决赛&#xff0c;比赛仍然采取…

基于java springboot的图书管理系统设计和实现

基于java springboot的图书管理系统设计和实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源码联系方式 文章目录…

load_dataset加载huggingface数据集失败

1. 一般的加载方式 from datasets import load_dataset dataset_dict load_dataset(cmrc2018)这种加载方式可能会显示因为连接问题导致失败&#xff0c;此时可以在hugging face里面找到对应的页面下载下来 然后改一下代码&#xff1a; from datasets import load_dataset d…

关于TFTP传输协议

TFTP&#xff08;Trivial File Transfer Protocol,简单文件传输协议&#xff09;&#xff1a;实现客户端与服务器之间简单文件传输。小文件传输&#xff0c;端口&#xff1a;69。协议简单&#xff0c;易于实现。 缺点&#xff1a; 传输效率低对于超时机制没有明确说明每包长度…