SpringBoot之定时任务

1. 前言

        本篇博客是个人的经验之谈,不是普适的解决方案。阅读本篇博客的朋友,可以参考这里的写法,如有不同的见解和想法,欢迎评论区交流。如果此篇博客对你有帮助,感谢点个赞~

2. 场景

        我们讨论在单体项目,单个实例中的定时任务相关问题。暂时先不讨论单体项目多副本的情况,也不讨论分布式定时任务。针对分布式定时任务,下一篇博客中再详细讨论。

        开发中遇到的场景是:一个单体项目,就比如一个后台管理系统需要多个定时任务去做一些业务处理。比如如下两个定时任务(以下定时任务是随便写的,目的是模拟系统中存在不同的定时任务,不需要纠结任务的合理性):

        (1)每隔10秒从网络上获取某些产品信息;

        (2)每隔3秒统计新注册的用户;

3. 需求实现

3.1 实现方式一

3.1.1 代码

        有了如上两个定时任务的需求,首先想到了SpringBoot中可以使用 @Scheduled 快速开启一个定时任务。代码如下:

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;@EnableScheduling
@Component
@Slf4j
public class MyJob {// 每隔2秒从网络上获取某些产品信息@Scheduled(cron = "*/2 * * * * ?")public void getProductInfo() throws InterruptedException {log.info("定时任务 - 从网络上获取某些产品信息");// 模拟业务耗时操作Thread.sleep(5000);}// 每隔3秒统计新注册的用户@Scheduled(cron = "*/3 * * * * ?")public void userJob() throws InterruptedException {log.info("定时任务 - 统计新注册的用户");// 模拟业务耗时操作Thread.sleep(1000);}}

 3.1.2 结果分析

        对上述定时任务的执行结果进行分析如下:

3.2 实现方式二

3.2.1 代码

        在上述 <实现方式一> 的基础上进行改进,给每次触发的定时任务分配一个线程去执行。注意:是每次触发定时任务时,给其分配一个线程,不要理解成给某个定时任务方法单独分配一个线程。再详细一些的解释:比如 "获取产品信息" 这个定时任务方法,每隔2秒触发一次,我们要实现的目标是:每隔2秒触发时,都为其分配一个线程去执行。

        要实现上述需求,可以定义一个线程池,每次定时任务触发时,从线程池中分配一个线程去执行当前的定时任务。我们可以把线程池的一些配置信息放到配置文件中,如下是自定义线程池的代码:

        3.2.1.1 线程池的配置

        在配置文件中指定线程池参数的配置信息,在实际生产环境中,如果需要修改线程池参数配置,修改配置文件即可。

# 自定义线程池相关配置(这里的线程池参数配置需要根据不同系统进行制定,这里只是一个示例)
custom:thread:core-pool-size: 20maximum-pool-size: 50queue-capacity: 10000keep-alive-time: 10name-prefix: "scheduled-thread-"
         3.2.1.2 配置类

        和配置文件中以 customer.thread 开头的配置信息进行绑定

package com.shg.distributed.lock.component;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;/*** @DESCRIPTION: 和自定义线程池相关的配置项,比如配置线程池的核心线程数* 最大线程数、阻塞队列的容量大小、非核心线程的存活时间等* @USER: shg* @DATE: 2024/06/24*/
@Data
@ConfigurationProperties(prefix = "custom.thread")
public class ThreadPoolConfigProperties {private int corePoolSize;private int maximumPoolSize;private int queueCapacity;private int keepAliveTime;private String namePrefix;}
        3.2.1.3  自定义线程池类
package com.shg.distributed.lock.component;import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;@Configuration
@EnableConfigurationProperties({ThreadPoolConfigProperties.class})
public class ScheduledThreadPool {private final ThreadPoolConfigProperties threadPoolConfigProperties;public ScheduledThreadPool(ThreadPoolConfigProperties threadPoolConfigProperties) {this.threadPoolConfigProperties = threadPoolConfigProperties;}@Bean(name = "asyncServiceExecutor")public Executor asyncServiceExecutor() {//阿里巴巴编程规范:线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。//SpringBoot项目,可使用Spring提供的对 ThreadPoolExecutor 封装的线程池 ThreadPoolTaskExecutor:ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();//配置核心线程数executor.setCorePoolSize(threadPoolConfigProperties.getCorePoolSize());//配置最大线程数executor.setMaxPoolSize(threadPoolConfigProperties.getMaximumPoolSize());//配置队列大小executor.setQueueCapacity(threadPoolConfigProperties.getQueueCapacity());// 非核心线程的存活时间executor.setKeepAliveSeconds(threadPoolConfigProperties.getKeepAliveTime());//配置线程池中的线程的名称前缀executor.setThreadNamePrefix(threadPoolConfigProperties.getNamePrefix());// rejection-policy:当pool已经达到max size的时候,如何处理新任务//     1、CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行。//        "该策略既不会抛弃任务,也不会抛出异常,而是将任务回推到调用者。"顾名思义,在饱和的情况下,调用者会执行该任务(而不是由多线程执行)//     2、AbortPolicy:拒绝策略,直接拒绝抛出异常//executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());//执行初始化executor.initialize();return executor;}
}
3.2.1.4 主启动类 

        主启动类上主要是标注:@EnableAsync 和 @EnableScheduling注解

package com.shg.distributed.lock;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableAsync
@EnableScheduling
@SpringBootApplication
public class DistributedLockApplication {public static void main(String[] args) {SpringApplication.run(DistributedLockApplication.class, args);}}

3.2.2 结果分析 

        对上述定时任务的执行结果分析如下:

4. 存在的问题

4.1 问题分析

        现在思考一个问题:"获取产品" 这个定时任务触发后,执行业务需要耗时5秒钟,但是定时任务是每隔2秒执行一次。如果每次定时任务被触发执行时,都为其分配一个线程。则会出现如下情况:

(1)15:21:50秒时刻触发一次定时任务,从线程池中拿一个线程 T1 开始处理业务逻辑;


(2)15:21:52秒时刻又触发执行定时任务,从线程池中拿一个线程 T2 开始处理业务逻辑(此时肯定拿不到线程 T1,因为 T1 线程还在处理第一个定时任务);


(3)15:21:54秒时刻又触发执行定时任务,从线程池中拿一个线程 T3 开始处理业务逻辑(此时肯定拿不到线程 T1和T2,因为 T1 和 T2线程都在处理各自的定时任务中的业务逻辑);


(4)15:21:55秒时刻,T1线程的业务逻辑处理完毕,T1线程释放,归还给线程池


(5)15:21:56秒时刻又触发执行定时任务,从线程池中拿一个线程 T4 开始处理业务逻辑(此时肯定拿不到线程T2 和 T3, 因为 T2 和 T3线程都在处理各自的定时任务中的业务逻辑);

        代码示例和输出结果如下:

4.2 问题解决 

        上面分析了 <当定时任务触发的时间间隔比处理业务耗时要小> 这种情况。大白话解释就是:上一个定时任务还没执行完成,下一个定时任务又开始了。

        而在实际的业务逻辑中,当一个定时任务触发执行后,下一次定时任务需要等到上一个定时任务执行完毕之后,才能开始执行。

        此时,我们就可以在定时任务触发时,为其加一把锁,如果成功获取到锁,则开始执行,执行完毕之后,就释放锁。如果在一个定时任务执行过程中,又触发了一次定时任务,此时是获取不到锁的,这个定时任务就不会执行业务逻辑了。

        进一步说明加锁后的效果,让打印信息更详细一些,注意图中每对【开始】-【结束】之间相差刚好是5秒。如下图: 

5. 总结

        以上我们从一个需求出发,讨论了
        (1)直接使用@Scheduled注解开启一个定时任务的方式及其遇到的问题;
        (2)接着针对(1)中的问题,我们自定义了线程池,让每次触发的定时任务,在不同的线程中执行,避免了一个项目中多个定时任务都在同一个线程中执行,导致定时任务阻塞的问题;
        (3)针对(2),如果一个定时任务没有执行完毕,下一个定时任务又开启了这种不合理的逻辑,我们通过简单的加锁方式解决了此问题。

        但是要注意,这里我们只是讨论并解决了 【单体项目-部署单个实例】中的定时任务的一些问题,并没有解决【单体项目-多副本实例】和【分布式项目中定时任务】的定时任务相关问题。
接下来的博客会对【单体项目-多副本实例】和【分布式项目中定时任务】进行讨论。主要讨论如下问题:

        (1)如何给定时任务添加分布式锁;
        (2)如何给定时任务添加的分布式锁进行续期;
        (3)其他一些衍生问题...

如果此篇文章对你有些许启发和帮助,感谢点个赞~

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

绿色能源发展关键:优化风电运维体系

根据QYResearch调研团队最新发布的《全球风电运维市场报告2023-2029》显示&#xff0c;预计到2029年&#xff0c;全球风电运维市场的规模将攀升至307.8亿美元&#xff0c;并且在接下来的几年里&#xff0c;其年复合增长率&#xff08;CAGR&#xff09;将达到12.5%。 上述图表及…

前端 Canvas 绘画 总结

目录 一、使用案例 1、基础使用案例 2、基本案例改为直接JS实现 二、相关资料 1、API教程文档 2、炫酷案例 一、使用案例 1、基础使用案例 使用Canvas的基本步骤&#xff1a; 1、需要一个canvas标签 2、需要获取 画笔 对象 3、使用canvas提供的api进行绘图 <!--…

力扣排序455题(分发饼干)

假设你是一位很棒的家长&#xff0c;想要给你的孩子们一些小饼干。 但是&#xff0c;每个孩子最多只能给一块饼干。 对每个孩子 i&#xff0c;都有一个胃口值 g[i],这是能 让孩子们满足胃口的饼干的最小尺寸;并且每块饼 干j&#xff0c;都有一个尺寸 s[j]。如果 s[j]> g[i]&…

C语言 | Leetcode C语言题解之第537题复数乘法

题目&#xff1a; 题解&#xff1a; bool parseComplexNumber(const char * num, int * real, int * image) {char *token strtok(num, "");*real atoi(token);token strtok(NULL, "i");*image atoi(token);return true; };char * complexNumberMulti…

Android使用scheme方式唤醒处于后台时的App场景

场景&#xff1a;甲App唤醒处于后台时的乙App的目标界面Activity&#xff0c;且乙App的目标界面Activity处于最上层&#xff0c;即已经打开状态&#xff0c;要求甲App使用scheme唤醒乙App时&#xff0c;达到跟从桌面icon拉起App效果一致&#xff0c;不能出现只拉起了乙App的目标…

如何对接低价折扣相对稳定电影票渠道?

对接低价折扣电影票渠道需要经过一系列步骤&#xff0c;以确保能够为用户提供优惠且可靠的购票体验。以下是一个基本的对接流程&#xff1a; 1.市场调研&#xff1a; 调研市场上的电影票销售渠道&#xff0c;了解主要的电影票批发商和分销商。分析竞争对手的折扣电影票服务&a…

【上云拼团Go】如何在腾讯云双十一活动中省钱

1. 前言 双十一已经成为了全球最大的购物狂欢节&#xff0c;除了电商平台的优惠&#xff0c;云计算服务商也纷纷在这个期间推出了诱人的促销活动。腾讯云作为中国云计算的领军企业之一&#xff0c;每年双十一的活动都吸引了大量开发者、企业和个人用户参与。那么&#xff0c;在…

新能源企业在精益变革过程中可能会遇到哪些困难?

在绿色转型的浪潮中&#xff0c;新能源企业作为推动社会可持续发展的先锋力量&#xff0c;正加速迈向精益化管理的新阶段。然而&#xff0c;这条变革之路并非坦途&#xff0c;而是布满了未知与挑战。本文&#xff0c;天行健王春城老师将深入探讨新能源企业在精益变革过程中可能…

Maven的安装配置

文章目录 一、MVN 的下载二、配置maven2.1、更改maven/conf/settings.xml配置2.2、配置环境变量一、MVN 的下载 还是那句话,要去就去官网或者github,别的地方不要去下载。我们下载binaries/ 目录下的 cd /opt/server wget https://downloads.apache.org/maven/maven-3/3.9.6/…

OpenCV视觉分析之目标跟踪(10)估计两个点集之间的刚性变换函数estimateRigidTransform的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 计算两个2D点集之间的最优仿射变换 estimateRigidTransform 是 OpenCV 中的一个函数&#xff0c;用于估计两个点集之间的刚性变换&#xff08;即…

块存储、文件存储和对象存储详细介绍

块存储、文件存储和对象存储介绍 块存储&#xff1a;像跑车&#xff0c;因为它们都能提供快速的响应和高性能&#xff0c;适合需要即时数据访问的场景&#xff0c;比如数据库和虚拟化技术。 文件存储&#xff1a;像货车&#xff0c;因为它们都能承载大量货物&#xff08;文件&…

A019基于SpringBoot的校园闲置物品交易系统

&#x1f64a;作者简介&#xff1a;在校研究生&#xff0c;拥有计算机专业的研究生开发团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339; 赠送计算机毕业设计600…

基于YOLO11/v10/v8/v5深度学习的煤矿传送带异物检测系统设计与实现【python源码+Pyqt5界面+数据集+训练代码】

《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…

explain执行计划分析 ref_

这里写目录标题 什么是ExplainExplain命令扩展explain extendedexplain partitions 两点重要提示本文示例使用的数据库表Explain命令(关键字)explain简单示例explain结果列说明【id列】【select_type列】【table列】【type列】 【possible_keys列】【key列】【key_len列】【ref…

关于elementui el-radio 赋值问题

今天遇到这样的问题&#xff1a; 点击的时候&#xff0c;同时选中 照抄官网&#xff01; 后来发现了问题&#xff1a; 也就是说如果你的版本太低&#xff0c;就不能用value&#xff0c;而得用label&#xff0c;于是修改 <el-radio-group v-model"searchTime"&g…

微服务系列六:分布式事务与seata

目录 实验环境说明 前言 一、分布式事务问题与策略 1.1 分布式事务介绍 1.2 分布式事务解决策略分析 二、分布式事务解决方案 Seata 2.1 认识Seata 2.2 Seata的工作原理 2.3 部署Seata微服务 2.3.1 准备数据库表 2.3.2 准备配置文件 2.3.3 docker部署 2.4 微服务集…

OceanBase 安装使用详细说明

OceanBase 安装使用详细说明 一、系统环境要求二、安装OceanBase环境方案一:在线下载并安装all-in-one安装包方案二:离线安装all-in-one安装包安装前的准备工作三、配置OceanBase集群编辑配置文件部署和启动集群连接到集群集群状态和管理四、创建业务租户和数据库创建用户并赋…

如何使用 SSH 连接并管理你的 WordPress 网站

在当今数字化的世界里&#xff0c;网站的管理与维护至关重要。对于使用 WordPress 搭建网站的用户而言&#xff0c;掌握基本的 SSH&#xff08;安全壳&#xff09;命令能够极大地简化网站管理工作。本指南将向你介绍 SSH 的基本知识&#xff0c;并教你如何通过 SSH 连接和管理你…

核心数据类型转换

核心数据类型转换 前言 前几集我们简单做了三条我们前后端交互接口的约定&#xff0c;简单看了我们的proto文件的内容&#xff0c;简单介绍了我们的Protobuf&#xff0c;并将protobuffer引入了我们的项目之中。 那么这一集我们就要把我们protobuffer的proto文件里的核心数据…

深入学习指针(5)!!!!!!!!!!!!!!!

文章目录 1.回调函数是什么&#xff1f;2.qsort使用举例2.1使用qsort函数排序整形数据2.2使用sqort排序结构数据 3.qsort函数的模拟实现 1.回调函数是什么&#xff1f; 回调函数就是⼀个通过函数指针调⽤的函数。 如果你把函数的指针&#xff08;地址&#xff09;作为参数传递…