springboot的调度服务与异步服务实操

news2024/12/22 19:59:00

1.调度服务

1.1.JDK之ScheduledExecutorService

讲到调度任务,我们脑海里马上会想到ScheduledExecutorService。

ScheduledExecutorService是 Java java.util.concurrent 包中的一个接口,它继承自 ExecutorService 接口。它主要用于在给定的延迟后运行任务,或者定期地执行任务。这个接口提供了几种安排任务执行的方法,包括单次执行、定期执行和周期性执行。

以下是 ScheduledExecutorService 提供的一些关键方法:

  1. schedule(Callable<V> callable, long delay, TimeUnit unit): 安排所提交的 Callable 任务在指定的延迟后运行,返回一个 Future,代表任务的结果。

  2. schedule(Runnable command, long delay, TimeUnit unit): 安排所提交的 Runnable 任务在指定的延迟后运行。

  3. scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit): 安排所提交的 Runnable 任务在指定的初始延迟后首次启动,并且随后按指定的周期重复执行。

  4. scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit): 安排所提交的 Runnable 任务在指定的初始延迟后首次启动,并且随后在每次执行结束和下次执行开始之间都存在指定的延迟。

然而,如果采用了sprintboot,我们也可以直接采用springboot提供的调试线程池。这其中有一个最大的优势在于,可以利用spring的cron表达式,采用方法注解的方式,不用引入quartz第三方工具。

1.2.springboot使用调度线程池

我们尝试从源代码的角度,来看看springboot提供的调度线程池是怎么创建的。

首先,启用调度配置,启动类加上@EnableScheduling

springboot的自动配置类一般以AutoConfiguration作为后缀,采用模糊搜索ScheduleAutoConfiguration可以找到目标TaskSchedulingAutoConfiguration。

从截图我们可以看出,我们只需在application.yml加入以下的配置就可以启动了

spring:
  task:
    ## 定时任务(业务上定时任务量不多,2个足矣)
    scheduling:
      ## 线程池核心线程数量
      pool:
        size: 2
      ## 线程名称前线
      threadNamePrefix: common-scheduling-

spring对线程池进行二次封闭,最终调用的还是jdk的ThreadPoolExecutor类,我们在该类的构造函数打个断点,可以看到,pool.size参数已经被传参:

1.3.ThreadPoolExecutor核心参数与执行流程

这里有必要先介绍下 ThreadPoolExecutor类的几个函数参数及基本运行机制

构造函数参数:

  1. 核心线程数(Core Pool Size): 线程池中始终保持的线程数量,即使它们处于空闲状态。如果任务数量少于核心线程数,线程池会创建新的线程来处理任务,而不会立即回收这些线程。

  2. 最大线程数(Maximum Pool Size): 线程池中允许的最大线程数量。如果任务数量超过了核心线程数但小于最大线程数,且工作队列已满,线程池会创建新的线程来处理任务,直到达到最大线程数。

  3. 工作队列(Work Queue): 用于存放待执行任务的阻塞队列。当所有核心线程都在忙碌时,新的任务会被放入工作队列中等待执行。

  4. 线程工厂(Thread Factory): 用于创建新线程的工厂。它提供了一种方式来定制线程的创建过程,例如设置线程的名称、优先级、是否为守护线程等。

  5. 拒绝策略(Rejected Execution Handler): 当任务无法被线程池及时处理时(即当线程池已满,且工作队列已满),线程池会采用拒绝策略来处理新提交的任务。常见的拒绝策略包括:

    • AbortPolicy:抛出 RejectedExecutionException
    • CallerRunsPolicy:由调用者线程运行该任务。
    • DiscardPolicy:静默丢弃任务。
    • DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试再次提交当前任务。
  6. 保持活动时间(Keep-Alive Time): 非核心线程空闲时在终止前等待新任务的最长时间。如果线程池允许核心线程空闲,这个参数也适用于核心线程。

  7. 时间单位(Time Unit): 与保持活动时间配合使用的时间单位,例如 TimeUnit.SECONDS

执行流程:

  1. 如果线程池中的线程数量少于核心线程数,即使有空闲线程,线程池也会优先创建新线程来执行新的任务。
  2. 如果线程池中的线程数量达到核心线程数,新的任务会被放入工作队列等待执行。
  3. 如果工作队列已满且线程数量少于最大线程数,线程池会创建新的非核心线程来执行任务。
  4. 如果工作队列已满且线程数量达到最大线程数,新的任务会被拒绝,线程池会采用拒绝策略来处理。

1.4.自定义线程池

由于spingboot对调度任务线程池的参数支持有限,如果想定制自己的参数,可以注入自己的调度线程池,从代码可看出:

@Configuration
public class ThreadPoolConfig {

    @Bean
    public ScheduledExecutorService scheduledExecutorService() {
        return new ScheduledThreadPoolExecutor(2,
                new NamedThreadFactory("common-schedule"),
                new ThreadPoolExecutor.CallerRunsPolicy()) {
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                super.afterExecute(r, t);
            }
        };
    }

}

1.5.spring cron注解

Cron 表达式是一种用于描述定时任务触发时间的字符串表达式。它由多个时间字段组成,每个字段代表定时任务在特定时间单位上的触发条件。

1.5.1.cron表达式语法格式

秒 分 时 日 月 星期 年份

其中,每个时间字段都有对应的取值范围和特殊符号。下面是每个时间字段的详细说明:

1、秒(Seconds):取值范围为 0~59。例如,`0/5` 表示从0秒开始,每隔 5 秒触发一次,`*` 表示每秒都触发。

2、分钟(Minutes):取值范围为 0~59。例如,`0/5` 表示从0分钟开始,每隔 5 分钟触发一次,`*` 表示每分钟都触发。

3、小时(Hours):取值范围为 0~23。例如,`0/2` 表示从0小时开始,每隔 2 小时触发一次,`*` 表示每小时都触发。

4、日期(Day of Month):取值范围为 1~31。例如,`1,15` 表示每月的 1 日和 15 日触发,`*` 表示每天都触发。

5、月份(Month):取值范围为 1~12,也可以使用英文缩写 JAN、FEB、MAR 等。例如,`1,6` 表示一月和六月触发,`*` 表示每个月都触发。

6、 星期(Day of Week):取值范围为 1~7,1 表示星期日,2 表示星期一,以此类推,也可以使用英文缩写 SUN、MON、TUE 等。例如,`2-6` 表示星期一到星期五触发,`*` 表示每个星期都触发。

7、年份(Year):可选字段,表示触发条件的年份。例如,`2023` 表示在 2023 年触发,`*` 表示每年都触发。

除了取值范围,Cron 表达式还支持一些特殊符号,用于指定特定的触发条件,例如:

- 星号(*):代表所有可能的取值,表示不限制该时间字段的取值范围。
- 问号(?):仅在日期和星期字段中使用,表示不指定具体的取值,可以任意匹配。
- 斜线(/):表示间隔触发,例如在分钟字段中,"*/5" 表示每隔 5 分钟触发一次。
- 逗号(,):用于指定多个取值,例如在小时字段中,"1,3,5" 表示在第 1、3、5 小时触发。
- 减号(-):用于指定一个范围,例如在月份字段中,"3-6" 表示三月到六月触发。

L  : 表示最后,只能出现在星期和每月第几天域,如果在星期域使用1L,意味着在最后的一个星期日触发。  

 W  : 表示有效工作日(周一到周五),只能出现在每月第几日域,系统将在离指定日期的最近的有效工作日触发事件。注意一点,W的最近寻找不会跨过月份  

 LW  : 这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。  

 #  : 用于确定每个月第几个星期几,只能出现在每月第几天域。例如在1#3,表示某月的第三个星期日。

1.5.2.cron表达式示例

作用表达式
每隔5秒执行一次*/5  *  *  *  *  ?
每天中午12点执行一次0 0 12 * * ?

2024年的每天上午10:00执行一次
0 0 10 * * ? 2023
每天下午6点到下午6:59每分钟执行一次0 * 18 * * ?
每月的最后一个星期五上午10:30执行一次0 30 10 ? * 6L
每月的第4个星期五上午10:25执行一次0  25  10  ?  *  6#4
每天上午8点,下午1点,4点执行一次0  0  8,13,16  * * ?

2.异步任务

2.1.springboot配置

在软件开发中,有些任务比较耗时但又无需马上获得结果。一般地,这些任务我们可以采用独立线程池异步执行。如果程序基于springboot环境,我们有现成的工具可以使用。
首先,我们需要在程序启动入口类增加@EnableAsync。
借着,我们尝试从源代码的角度,来看看springboot提供的异步线程池是怎么创建的。
从springboot的命名风格可知,通过模糊搜索TaskAutoConfiguration,可以找到TaskExecutionAutoConfiguration,如下:

从源代码可知,只需在application.yml配置如下参数即可: 

spring:    
    execution:
      pool:
        coreSize: 4
        queueCapacity: 64
        maxSize: 8
        ## 禁止空闲线程关闭,保证最少有core个存活线程
        allowCoreThreadTimeout: false
        keepAlive: 300s
      threadNamePrefix: common-async_task-

这些参数跟jdk的ThreadPoolExecutor类的构造参数非常相似,这里不作解析 。

2.2.使用异步任务

使用方法,只要在目标方法的签名加上@Async

然而,执行结果却出乎意外(在main主线程上执行,没有异步执行

熟悉springaop机制的同学,马上知道这是因为异步任务底层是基于动态代理机制实现的。Spring AOP 代理只有在通过 Spring 容器获取 Bean 时才会创建。当在同一个类内部调用一个方法时,调用的是原始对象,而不是代理对象。因此,内部调用不会经过 Spring AOP 代理,也就无法触发异步执行。解决这个问题最简单的方法是用一个新的类来管理异步执行方法。

问题解决

2.3.模拟系统繁忙进行性能测试

我们尝试模拟一些极端情况,系统处理不过来的情况。修改上面的配置,改为

pool:
  coreSize: 1
  queueCapacity: 1
  maxSize: 8

同时,异步执行增加时间延迟来模拟耗时任务

@Component
@Slf4j
class AsyncTaskHandler {

    @Async
    public void busyTask1() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException ignored) {
        }
        log.info("----------busyTask1---------");
    }

    @Async
    public void busyTask2() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException ignored) {
        }
        log.info("----------busyTask2---------");
    }

}

执行结果会出现报错(线程数已达到最大数量,且任务队列已满,无法添加新任务)

 调度任务执行频率是可预见的,有多少个任务,执行频率,开发可知,核心数量,最大数量,队列容量比较好设定。而异步执行频率很大程度是由系统的使用者(用户)决定的,因此这些参数需要根据流量动态修改。

2.4.异步线程池拒绝策略

如果不想把  queueCapacity和maxSize都设置成很大的话,我们可以考虑修改下线程池的拒绝策略。最妥当的方式是,既然异步不了,那就"熔断"成同步,直接在调用者所在的业务线程执行。然而,springboot没有提供相应的配置项。为此,我们只能关闭springboot的自动配置了。 

从代码可看出,只要提供一个Executor实例,并且名字叫taskExecutor即可。

话不多说,上代码

@Configuration
public class ThreadPoolConfig {

    private final int core = Runtime.getRuntime().availableProcessors();

    /**
     * springboot 自动注入的异步执行线程池,拒绝策略为丢弃,难以配置maxPoolSize参数
     * @see TaskExecutionAutoConfiguration#taskExecutorBuilder
     */
    @Bean(name = "taskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor(TaskExecutionProperties properties) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(properties.getPool().getCoreSize());
        executor.setThreadNamePrefix(properties.getThreadNamePrefix());
        executor.setMaxPoolSize(properties.getPool().getMaxSize());
        executor.setQueueCapacity(properties.getPool().getQueueCapacity());
        executor.setKeepAliveSeconds((int) properties.getPool().getKeepAlive().toSeconds());
        executor.setAllowCoreThreadTimeOut(properties.getPool().isAllowCoreThreadTimeout());
        // 超过队列容量,则在业务线程上执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

}

重新运行程序,可以看到,当任务超过线程池负载的时候,多余的线程会在调用线程上执行,变为同步代码

个人认为:使用springboot创建的线程池,代码也只是稍微简化一点点。

采用原生线程池,每个业务代码必须实现Runnable接口,而使用springboot的异步线程池,只需以方法注解的形式即可,底层aop会生成对应的代理方法。但要确保避免内部方法调用导致异步逻辑失效。

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

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

相关文章

计算机毕业设计Python+Spark知识图谱微博舆情预测 微博推荐系统 微博可视化 微博数据分析 微博大数据 微博爬虫 Hadoop 大数据毕业设计

《PythonSpark知识图谱微博舆情预测》开题报告 一、课题背景与意义 随着互联网技术的飞速发展&#xff0c;社交媒体平台如微博已成为人们表达观点、交流信息的重要渠道。微博每天产生海量的数据&#xff0c;这些数据中蕴含着丰富的社会情绪、事件动态等信息&#xff0c;对于政…

从概念到使用全面了解Llama 3 这个迄今为止最强大的开源模型

概述 mate最近发布了 Llama3&#xff0c;下一代最先进的开源大语言模型&#xff08;LLM&#xff09;。正如本文的综合评论所述&#xff0c;Llama 3 以其前身奠定的基础为基础&#xff0c;旨在增强 Llama 2 成为 ChatGPT 重要开源竞争对手的能力 Llama 2&#xff1a;深入探讨 C…

【更新】红色文化之红色博物馆数据集(经纬度+地址)

数据简介&#xff1a;红色博物馆作为国家红色文化传承与爱国主义教育的重要基地&#xff0c;遍布全国各地&#xff0c;承载着丰富的革命历史与文化记忆。本数据说明旨在汇总并分析全国范围内具有代表性的红色博物馆的基本信息&#xff0c;包括其地址、特色及教育意义&#xff0…

Scrapy框架入门

一、Scrapy简介 Scrapy是一款快速而强大的web爬虫框架&#xff0c;基于Twisted的异步处理框架、Twisted是事件驱动的。 Scrapy是由python实现的爬虫框架&#xff1a;架构清晰、可扩展性强、可以灵活完成需求。 一、Scrapy应用 scrapy及其他模块的安装 pip3 install scrapy…

【Java编程系列】使用java接入Microsoft Graph,实现发送邮件功能

1、前言 微软与2022年10月1号&#xff0c;开始停止了部分服务的 basic auth &#xff08;账密登录&#xff09;功能&#xff0c;需要改用 oauth2.0 协议接入相应服务。邮件方面主要在于IMAP和pop协议。并且与2023年1月1日时&#xff0c;正式全面停止账密登录使用去接入上述服务…

【Python报错已解决】[notice] A new release of pip available: 22.2 -> 22.2.2

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

共享单车轨迹数据分析:以厦门市共享单车数据为例(九)

副标题&#xff1a;基于站点800m范围内评价指标探究——以吕厝站为例 上篇文章我们以厦门市为例&#xff0c;来通过POI和优劣解距离法&#xff08;TOPSIS&#xff09;来研究厦门岛内以800m作为辐射范围的地铁站哪些地铁站发展的最好&#xff0c;根据综合得分指数可以知道&…

架构设计笔记-5-软件工程基础知识-2

知识要点 构件组装是将库中的构件经适当修改后相互连接,或者将它们与当前开发项目中的软件元素连接,最终构成新的目标软件。 构件组装技术大体可分为: 1. 基于功能的组装技术:基于功能的组装技术采用子程序调用和参数传递的方式将构件组装起来。它要求库中的构件以子程序…

KPaaS集成平台与传统集成有什么不同?

企业的业务系统越来越复杂&#xff0c;集成需求也日益增长&#xff0c;同时也面临着如何高效、低成本地实现多系统集成的挑战。传统集成方式虽然能够提供高度定制化的解决方案&#xff0c;但其高昂的成本、复杂的实施过程以及后续的维护难题却让许多企业望而却步。在这样的背景…

【Python报错已解决】KeyError: ‘key‘

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

Nginx基础详解4(location模块、nginx跨域问题的解决、nginx防盗链的设计原理及应用、nginx模块化解剖)

续Nginx基础详解3&#xff08;nginx.conf核心代码讲解、常用命令解析、Nginx日志切割&#xff09;-CSDN博客 目录 10.location模块详解 10.1默认匹配规则‘ /’ 10.2精确匹配规则‘/’ 10.3正则表达式‘~* \’ 10.4精确正则匹配~ \ 10.5路径匹配‘^~ /’ 11.nginx中解决…

深度解析:Python蓝桥杯青少组精英赛道与高端题型概览

目录 一、蓝桥杯青少组简介二、赛项组别与年龄范围三、比赛内容与题型1. 基础知识范围2. 题型设置2.1 选择题2.2 编程题 3. 考试时长 四、奖项设置与激励措施五、总结 一、蓝桥杯青少组简介 蓝桥杯全国软件和信息技术专业人才大赛&#xff08;简称“蓝桥杯”&#xff09;是由工…

“等保测评实践:提升企业网络安全管理水平“

随着信息技术的快速发展&#xff0c;网络安全问题日益成为企业关注的焦点。等级保护&#xff08;简称等保&#xff09;测评作为提升企业网络安全管理水平的重要手段&#xff0c;其在数字化转型的今天显得尤为重要。本文将探讨如何通过等保测评实践&#xff0c;构建企业网络安全…

PWM 模式

一、介绍 PWM&#xff08;脉宽调制&#xff0c;Pulse-width modulation&#xff09;是一种通过调节脉冲信号的宽度来控制电能输出的方法。PWM是一种方波信号&#xff0c;通常在电子和电气工程中用于调节功率输送&#xff0c;控制电机速度&#xff0c;调节LED亮度&#xff0c;以…

数据结构-3.7.双端队列

一.双端队列的三种形式&#xff1a; 双端队列也可以是只在一端删除和添加&#xff0c;此时就是栈&#xff1b; 双端队列在一端添加&#xff0c;另一端输出&#xff0c;此时就是队列&#xff1b; 二.判断输出序列合法性&#xff1a; 题目&#xff1a;若数据元素输入序列为1&am…

AI产品经理PRD文档与传统产品经理PRD有什么不同呢?

目录 模型输出&#xff1a;说白了&#xff0c;就是你的AI要干啥数据接入&#xff1a;你的AI要吃啥“粮食”验收标准&#xff1a;怎么判断你的AI干得好不好经验总结 你好&#xff0c;我是三桥君 在工作中&#xff0c;当我作为传统产品经理时&#xff0c;通常只需提供产品需求文…

数据结构-3.10.队列的应用

一.树的层次遍历&#xff1a; 新建队列&#xff0c;之后&#xff1a; 首先根节点1入队列&#xff0c;之后他的两个子节点2&#xff0c;3入队列&#xff0c;最后1就可以出队列了&#xff1a; 遍历2&#xff0c;他的两个子节点4&#xff0c;5入队列&#xff0c;最后2就可以出队列…

《论文阅读》PECER:通过动态人格提取和情境情绪推理产生同理心反应 ICASSP 2024

《论文阅读》PECER:通过动态人格提取和情境情绪推理产生同理心反应 ICASSP 2024 前言简介任务定义模型架构Cognitive-Affective Personality PerceiverMulti-source EncoderInteractive Decoder损失函数实验结果可持续发展观点前言 亲身阅读感受分享,细节画图解释,再也不用…

Linux 再入门整理:详解 /etc/fstab 文件

目录 1. 什么是 /etc/fstab2. /etc/fstab 文件的格式2.1 设备文件 (Device)2.2 挂载点 (Mount Point)2.3 文件系统类型 (File System Type)2.4 挂载选项 (Mount Options)2.5 Backup Operation&#xff08;dump 参数&#xff09;2.6 Pass Order (fsck 参数)2.6.1 参数设置2.6.2 …

Linux 进程的基本概念及描述

目录 0.前言 1. 什么是进程 1.1 进程的定义与特性 1.2 进程与线程的区别 2.描述进程 2.1 PCB (进程控制块) 2.2 task_struct 3.查看进程 3.1 查看进程信息 3.1.1 /proc 文件系统 3.1.2 ps 命令 3.1.2 top 和 htop 命令 3.2 获取进程标识符 3.2.1使用命令获取PID 3.2.2 使用C语言…