SpringBoot 如何执行定时任务

SpringBoot 如何执行定时任务

工作中有需要应用到定时任务的场景,一天一次,一周一次,一月一次,一年一次,做日报,周报,月报,年报的统计,以及信息提醒,等,spring boot 提供了一个两种方式实现定时任务。

一、静态定时任务—基于注解

SpringBoot 中的 @Scheduled 注解为定时任务提供了一种很简单的实现,只需要在注解中加上一些属性,例如 fixedRatefixedDelaycron(最常用)等等,并且在启动类上面加上 @EnableScheduling 注解,就可以启动一个定时任务了。基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。

程序中若需要在某个时间循环执行某项任务,就可以使用@Scheduled定时器

1、主要涉及的注解:

@EnableScheduling   //开启定时任务,在配置类上使用,为了方便,可以用在项目启动类上

@Scheduled  //执行任务间隔设置,来声明这是一个任务,包括 cron,fixDelay,fixRate 等类型
(1)开启定时任务

SpringBoot 项目在项目启动类上添加 @EnableScheduling 注解即可开启定时任务管理。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
 
@SpringBootApplication
@EnableScheduling //开启定时任务
public class ScheduledDemoApplication{
    public static void main(String[] args){
        SpringApplication.run(ScheduledDemoApplication.class, args);
    }
}
(2)创建定时任务

创建定时任务,并使用 @Scheduled 注解。

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
 
import java.text.SimpleDateFormat;
import java.util.Date;
 
/**
 * 定时任务的使用
 **/
@Component //注入到容器
public class Task{
    @Scheduled(cron="0/5 * *  * * ? ")   //每5秒执行一次
    public void execute(){
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //设置日期格式
        System.out.println("执行第"+ i++ +"次定时任务_" + df.format(new Date()));
    }
}

可通过在线生成Cron表达式的工具:http://cron.qqe2.com/ 来生成自己想要的表达式。

以上,基于@Scheduled 注解来实现定时任务已完成;以下介绍@Scheduled 注解各个参数的使用

2、@Scheduled注解各大参数的作用

发现有这么几个参数:cronzonefixedDelayfixedDelayStringfixedRatefixedRateStringinitialDelayinitialDelayString 用法分别如下:

@Scheduled注解类的源码

package org.springframework.scheduling.annotation;
 
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
    String CRON_DISABLED = "-";
 
    String cron() default "";
 
    String zone() default "";
 
    long fixedDelay() default -1L;
 
    String fixedDelayString() default "";
 
    long fixedRate() default -1L;
 
    String fixedRateString() default "";
 
    long initialDelay() default -1L;
 
    String initialDelayString() default "";
}
2.1、cron相关参数意义

一个cron表达式有至少6个(也可能7个)有空格分隔的时间元素。

按顺序依次为

  • 秒(0~59) 例如0/5表示每5秒
  • 分钟(0~59)
  • 小时(0~23)
  • 天(月)(0~31,但是你需要考虑你月的天数)
  • 月(0~11)
  • 天(星期)(1~7 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT)
  • 年份(1970-2099)——@Scheduled是不支持的,spring quartz支持
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 12 ? * WED 表示每个星期三中午12点
“0 0 12 * * ?” 每天中午12点触发
“0 15 10 ? * *” 每天上午10:15触发
“0 15 10 * * ?” 每天上午10:15触发
“0 15 10 * * ? *” 每天上午10:15触发
“0 15 10 * * ? 2005” 2005年的每天上午10:15触发
“0 * 14 * * ?” 在每天下午2点到下午2:59期间的每1分钟触发
“0 0/5 14 * * ?” 在每天下午2点到下午2:55期间的每5分钟触发
"0 “0/5 14,18 * * ?” 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
“0 0-5 14 * * ?” 在每天下午2点到下午2:05期间的每1分钟触发
“0 10,44 14 ? 3 WED” 每年三月的星期三的下午2:10和2:44触发
“0 15 10 ? * MON-FRI” 周一至周五的上午10:15触发
“0 15 10 15 * ?” 每月15日上午10:15触发
“0 15 10 L * ?” 每月最后一日的上午10:15触发
“0 15 10 ? * 6L” 每月的最后一个星期五上午10:15触发
“0 15 10 ? * 6L 2002-2005” 2002年至2005年的每月的最后一个星期五上午10:15触发
"0 15 10 ? “* 6#3” 每月的第三个星期五上午10:15触发

有些子表达式能包含一些范围或列表

例如:子表达式(天(星期))可以为 “MON-FRI”,“MON,WED,FRI”,“MON-WED,SAT”

“*”字符代表所有可能的值

因此,“”在子表达式(月)里表示每个月的含义,“”在子表达式(天(星期))表示星期的每一天

“/”字符用来指定数值的增量

例如:在子表达式(分钟)里的“0/15”表示从第0分钟开始,每15分钟

在子表达式(分钟)里的“3/20”表示从第3分钟开始,每20分钟(它和“3,23,43”)的含义一样

“?”字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值

当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为“?”

“L” 字符仅被用于天(月)和天(星期)两个子表达式,它是单词“last”的缩写

但是它在两个子表达式里的含义是不同的。

在天(月)子表达式中,“L”表示一个月的最后一天

在天(星期)自表达式中,“L”表示一个星期的最后一天,也就是SAT

如果在“L”前有具体的内容,它就具有其他的含义了

例如:“6L”表示这个月的倒数第6天,“FRIL”表示这个月的最一个星期五

注意:在使用“L”参数时,不要指定列表或范围,因为这会导致问题

字段允许值允许的特殊字符
0-59, - * /
0-59, - * /
小时0-23, - * /
日期1-31, - * ? / L W C
月份1-12 或者 JAN-DEC, - * /
星期1-7 或者 SUN-SAT, - * ? / L C #
年(可选)留空, 1970-2099, - * /

局限性——@Scheduled的cron无法指定执行的年份

即我们假如使用下面的定时任务

@Scheduled(cron = "0 18 10 * * ? 2021-2022")
public void testTaskWithDate() {
	logger.info("测试2021.定时任务");
}

将会报下面的错误

Cron expression must consist of 6 fields (found 7 in "0 18 10 * * ? 2016-2016")

cron表达式使用占位符

另外,cron属性接收的cron表达式支持占位符。

配置文件:

time:
  cron: */5 * * * * *
  interval: 5

每5秒执行一次:

@Scheduled(cron="${time.cron}")
void testPlaceholder1() {
    System.out.println("Execute at " + System.currentTimeMillis());
}
 
@Scheduled(cron="*/${time.interval} * * * * *")
void testPlaceholder2() {
    System.out.println("Execute at " + System.currentTimeMillis());
}
2.2、zone

时区,接收一个 java.util.TimeZone#IDcron表达式会基于该时区解析。默认是一个空字符串,即取服务器所在地的时区。比如我们一般使用的时区Asia/Shanghai。该字段我们一般留空。

2.3、fixedDelayfixedDelayString

这两个参数意思是相同的,都是表示:上一次执行完毕时间点之后多长时间再执行,区别是:后者支持占位符。

上一次执行完毕时间点之后多长时间再执行。如:

@Scheduled(fixedDelay = 5000) //上一次执行完毕时间点之后5秒再执行

@Scheduled(fixedDelayString = "5000") //上一次执行完毕时间点之后5秒再执行

fixedDelayString使用占位符

#配置文件
time:
  fixedDelay: 5000
@Scheduled(fixedDelayString = "${time.fixedDelay}")

2.4、fixedRatefixedRateString

这两个参数意思是相同的,都是表示:上一次开始执行时间点之后多长时间再执行,区别是:后者支持占位符。

上一次开始执行时间点之后多长时间再执行。如:

@Scheduled(fixedRate = 5000) //上一次开始执行时间点之后5秒再执行
@Scheduled(fixedRateString = "5000") //上一次开始执行时间点之后5秒再执行

fixedRateString使用占位符

#配置文件
time:
  fixedDelay: 5000
@Scheduled(fixedRateString = "${time.fixedDelay}")
2.4、initialDelayinitialDelayString

这两个参数意思是相同的,都是表示:第一次延迟多长时间后再执行,区别是:后者支持占位符。

@Scheduled(initialDelay=1000, fixedRate=5000) //第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次

initialDelayString使用占位符 同上

二、动态定时任务—基于接口

不需要重启项目就能够生效,需要把cron表达式持久化到数据库

1、持久化到数据库

1.1、创建数据表

MySQL数据库中创建cron 表,并添加数据。

DROP TABLE IF EXISTS cron;
CREATE TABLE cron  (
  cron_id VARCHAR(30) NOT NULL PRIMARY KEY,
  cron VARCHAR(30) NOT NULL  
);
 
INSERT INTO cron VALUES ('1', '0/5 * * * * ?');
1.2、添加pom.xml配置信息
<!-- MyBatis与SpringBoot整合依赖 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
 
<!-- MySQL的JDBC数据库驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.20</version>
</dependency>
1.3、配置相关信息

将项目默认的application.properties文件的后缀修改为“.yml”,即配置文件名称为:application.yml,并配置以下信息:

spring:
  #DataSource数据源
  datasource:
    url: jdbc:mysql://localhost:3306/db_admin?useSSL=false&amp
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
 
#MyBatis配置
mybatis:
  type-aliases-package: com.pjb.entity #别名定义
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #指定 MyBatis 所用日志的具体实现,未指定时将自动查找
    map-underscore-to-camel-case: true #开启自动驼峰命名规则(camel case)映射
    lazy-loading-enabled: true #开启延时加载开关
    aggressive-lazy-loading: false #将积极加载改为消极加载(即按需加载),默认值就是false
    lazy-load-trigger-methods: "" #阻挡不相干的操作触发,实现懒加载
    cache-enabled: true #打开全局缓存开关(二级环境),默认值就是true

2、创建定时器

数据库准备好数据之后,我们编写定时任务,注意这里添加的是TriggerTask,目的是循环读取我们在数据库设置好的执行周期,以及执行相关定时任务的内容。具体代码如下:

package com.example.springSchedule.controller;

import com.example.springbucket.mapper.CronMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;

/**
 * 动态定时任务配置类
 */
@Configuration //标记配置类,兼备Component的效果
@EnableScheduling //开启定时任务
public class SimpleScheduleConfig implements SchedulingConfigurer{

    @Autowired
    CronMapper cronMapper;

//    @Override
//    public void configureTasks(ScheduledTaskRegistrar taskRegistrar){
//        taskRegistrar.addTriggerTask(
//                //添加任务内容(Runnable)
//                () -> System.out.println("线程启动:" + LocalDateTime.now().toLocalTime()),
//                //设置执行周期(Trigger)
//                triggerContext -> {
//                    //从数据库获取执行周期
//                    String cron = cronMapper.getCron();
//                    //非空校验
//                    if(StringUtils.isEmpty(cron)){
//					  //TODO
//                    }
//                    //返回执行周期
//                    return new CronTrigger(cron).nextExecutionTime(triggerContext);
//                }
//        );
//
//    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar){
        Thread thread = new Thread(){
            @Override
            public void run() {
                super.run();
                System.out.println("定时发送线程启动:" + LocalDateTime.now().toLocalTime());
            }
        };

        taskRegistrar.addTriggerTask(
                thread,
                triggerContext -> {
                    //从数据库获取执行周期
                    String cron = cronMapper.getCron();
                    //非空校验
                    if(StringUtils.isEmpty(cron)){
					//TODO
                    }
                    //返回执行周期
                    return new CronTrigger(cron).nextExecutionTime(triggerContext);
                }
        );

    }
}

通过读取数据库数据来控制定时任务的周期

注意: 如果在数据库修改时格式出现错误,则定时任务会停止,即使重新修改正确;此时只能重新启动项目才能恢复。

因为SchedulingConfigurer类没有直接提供关闭定时任务的方法,所以,当我们需要停止定时任务的循环执行的时候,清空cron表达式,通过非空校验捕获到,然后业务处理手动关闭的功能。

//清空扫描到的定时任务即可
taskRegistrar.setTriggerTasks(Maps.newHashMap());
taskRegistrar.setCronTasks(Maps.newHashMap());
taskRegistrar.setFixedRateTasks(Maps.newHashMap());
taskRegistrar.setFixedDelayTasks(Maps.newHashMap());

三、动态控制定时任务开启与关闭

例如项目部署上线之后,我们可能会修改定时任务的执行时间,并且停止、重启定时任务等,因为定时任务是直接写死在程序中的,修改起来不是非常的方便。所以,简单记录一下自己的一些解决方案,仅供参考。

1、针对 @Scheduled 定时任务

cron 表达式配在 application.yml 中:

#application.yml中的配置
scheduled:
  cron: 0/5 * * * * ?

在 @Scheduled 中获取这个配置:

@Component
public class TestTask {
    private static SimpleDateFormat dateFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Scheduled(cron = "${scheduled.cron}")
	public void test(){
    	System.out.println(dateFmt.format(new Date()) + " : 执行定时任务");
	}

}
1.1、如何关闭定时任务

Spring Boot 2.1 以上的版本还提供了一种停止定时任务的方案,就是在cron中配置 “-” 即可,你也可以在配置文件中设置这个符号:

#application.yml中的配置
scheduled:
  cron: "-"

注意这里必须加上一个双引号,因为在 application.yml 中, - 是一个特殊的字符。

1.2、为定时任务设置开关

另一种方式,在配置文件中配置一个 boolean 属性,如果是 true 的话,就开启定时任务,否则不开启。

#application.yml中的配置
scheduled:
  cron: 0/5 * * * * ?
enable:
  scheduled: true # @Schedule 定时任务的开true/关false

其实 @Scheduled 注解,是被一个叫做 ScheduledAnnotationBeanPostProcessor 的类所拦截的,所以我们可以根据配置,决定是否创建这个 bean,如果没有这个 bean,@Scheduled 就不会被拦截,那么定时任务肯定不会执行了

**注意:这种方式,启动类上面的 @EnableScheduling 需要去掉。**不去除定时将一直生效,一直为true

(1)、创建一个 ScheduledCondtion
public class ScheduledCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //读取配置中的属性
        return Boolean.parseBoolean(context.getEnvironment().getProperty("enable.scheduled"));
    }
}

这个类的功能很简单,就是去读取配置,然后返回一个 boolean 值。

(2)、创建一个 ScheduledConfig 配置类

需要 @Conditional注解来实现这个功能,详细了解见:

@Configuration
public class ScheduledConfig {
    // 根据配置文件中的内容,决定是否创建 bean
    @Conditional(ScheduledCondition.class)
    @Bean
    public ScheduledAnnotationBeanPostProcessor processor() {
        return new ScheduledAnnotationBeanPostProcessor();
    }
}

这个配置,就是以 ScheduledCondtion 为条件,决定是否创建 bean。然后,启动项目,定时任务就会执行,如果我们将配置修改为 false,则不会执行。

这样的话,我们就能够很容易的启动或者关闭定时任务了,并且也可以实时修改 cron表达式的值。

2021-01-27 11:14:35 : 执行定时任务
2021-01-27 11:14:40 : 执行定时任务
2021-01-27 11:14:45 : 执行定时任务
2021-01-27 11:14:50 : 执行定时任务
2021-01-27 11:14:55 : 执行定时任务
2021-01-27 11:15:00 : 执行定时任务

2、针对基于接口方式修改定时任务且不重启项目

基于接口 SchedulingConfigurer,这里大家要了解ScheduledTaskRegistrar 这个类

2.1、建表 管理定时任务
DROP TABLE IF EXISTS `scheduled`;
CREATE TABLE `scheduled` (
  `name` varchar(20) DEFAULT NULL,
  `cron` varchar(30) DEFAULT NULL,
  `open` tinyint(1) DEFAULT NULL COMMENT '1开启, 其他为关闭'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-- ----------------------------
-- Records of scheduled
-- ----------------------------
INSERT INTO `scheduled` VALUES ('demo1', '0/5 * * * * ?', '1');
INSERT INTO `scheduled` VALUES ('demo2', '0/10 * * * * ?', '1');
2.1、实现定时任务配置类
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/**
 * 基于接口SchedulingConfigurer的动态定时任务.
 */
@Configuration
@EnableScheduling
public abstract class BaseSchedulingConfigurer implements SchedulingConfigurer {

    /**
     * 定时任务周期表达式.
     */
    private String cron;

    /**
     * 重写配置定时任务的方法.
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.setScheduler(taskScheduler());
        scheduledTaskRegistrar.addTriggerTask(
                //执行定时任务
                this::taskService,
                //设置触发器
                triggerContext -> {
                    //获取定时任务周期表达式
                    cron = getCron();
                    //返回执行周期
                    return new CronTrigger(cron).nextExecutionTime(triggerContext);
                }
        );
    }

    @Bean
    public Executor taskScheduler() {
        //设置线程名称
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("scheduler-pool-%d").build();
        //创建线程池
        return Executors.newScheduledThreadPool(3, namedThreadFactory);
    }

    /**
     * 执行定时任务
     */
    public abstract void taskService();

    /**
     * 获取定时任务周期表达式
     */
    public abstract String getCron();

}
2.2、创建定时任务
定时任务一:
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.dao.CronMapper;
import com.example.demo.entity.Scheduled;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
 
import java.time.LocalDateTime;
 
/**
 * 定时任务一.
 */
@Configuration
public class TaskDemo extends BaseSchedulingConfigurer {
 
    /**
     * 注入mapper
     */
    @Autowired      
    @SuppressWarnings("all")
    CronMapper cronMapper;
 
    /**
     * 执行定时任务内容
     */
    @Override
    public void taskService() {
        Integer open = getOpen();
        if (1== open){
            System.out.println("定时任务demo1:"
                    + LocalDateTime.now()+",线程名称:"+Thread.currentThread().getName()
                    + " 线程id:"+Thread.currentThread().getId());
        }
    }
 
    /**
     * 获取定时任务执行周期表达式
     */
    @Override
    public String getCron() {
        QueryWrapper<Scheduled> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name","demo1");
        String cron = cronMapper.selectOne(queryWrapper).getCron();
        return cron;
    }
 
    /**
     * 得到定时任务,开关状态.
     */
    public Integer getOpen() {
        QueryWrapper<Scheduled> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", "demo1");
        Integer open = cronMapper.selectOne(queryWrapper).getOpen();
        return open;
    }
 
}
定时任务二:
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.dao.CronMapper;
import com.example.demo.entity.Scheduled;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
 
import java.time.LocalDateTime;
 
/**
 * 定时任务二.
 */
@Configuration
public class TaskDemoCopy extends BaseSchedulingConfigurer {
 
    /**
     * 注入mapper
     */
    @Autowired      
    @SuppressWarnings("all")
    CronMapper cronMapper;
 
    /**
     * 执行定时任务内容
     */
    @Override
    public void taskService() {
        Integer open = getOpen();
        if (1== open){
            System.out.println("定时任务demo2:"
                    + LocalDateTime.now()+",线程名称:"+Thread.currentThread().getName()
                    + " 线程id:"+Thread.currentThread().getId());
        }
    }
 
    /**
     * 获取定时任务执行周期表达式
     */
    @Override
    public String getCron() {
        QueryWrapper<Scheduled> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name","demo2");
        String cron = cronMapper.selectOne(queryWrapper).getCron();
        return cron;
    }
 
    /**
     * 得到定时任务,开关状态.
     */
    public Integer getOpen() {
        QueryWrapper<Scheduled> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", "demo2");
        Integer open = cronMapper.selectOne(queryWrapper).getOpen();
        return open;
    }
}
结果打印

定时任务demo1 :每五秒执行一次

定时任务demo2: 每十秒执行一次

定时任务demo1:2021-01-27T17:45:10.001,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T17:45:10.001,线程名称:scheduler-pool-1 线程id:29
定时任务demo1:2021-01-27T17:45:15.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo1:2021-01-27T17:45:20.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T17:45:20.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo1:2021-01-27T17:45:25.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo1:2021-01-27T17:45:30.001,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T17:45:30.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo1:2021-01-27T17:45:35.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T17:45:40.001,线程名称:scheduler-pool-0 线程id:28

修改数据库 demo1的执行时间为每15秒一次:

定时任务demo1 :每十五秒执行一次

定时任务demo2: 每十秒执行一次

定时任务demo1:2021-01-27T17:58:05.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo2:2021-01-27T17:58:10.001,线程名称:scheduler-pool-2 线程id:51
定时任务demo1:2021-01-27T17:58:10.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo1:2021-01-27T17:58:15.001,线程名称:scheduler-pool-1 线程id:29
定时任务demo2:2021-01-27T17:58:20.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo1:2021-01-27T17:58:30.003,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T17:58:30.003,线程名称:scheduler-pool-1 线程id:29
定时任务demo2:2021-01-27T17:58:40.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo1:2021-01-27T17:58:45.001,线程名称:scheduler-pool-2 线程id:51

关闭demo1的定时任务,将demo1的open状态改为0:

定时任务demo1 :关闭

定时任务demo2: 每十秒执行一次

定时任务demo2:2021-01-27T17:59:50.003,线程名称:scheduler-pool-0 线程id:28
定时任务demo2:2021-01-27T18:00:00.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo1:2021-01-27T18:00:00.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo2:2021-01-27T18:00:10.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo1:2021-01-27T18:00:15.001,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T18:00:20.001,线程名称:scheduler-pool-0 线程id:28
定时任务demo2:2021-01-27T18:00:30.001,线程名称:scheduler-pool-1 线程id:29
定时任务demo2:2021-01-27T18:00:40.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo2:2021-01-27T18:00:50.001,线程名称:scheduler-pool-0 线程id:28

重新打开demo1的定时任务,将demo1的open状态改为1:

定时任务demo2:2021-01-27T18:04:00.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T18:04:10.001,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T18:04:20.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo2:2021-01-27T18:04:30.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo1:2021-01-27T18:04:30.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T18:04:40.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo1:2021-01-27T18:04:45.002,线程名称:scheduler-pool-1 线程id:29
定时任务demo2:2021-01-27T18:04:50.002,线程名称:scheduler-pool-2 线程id:51
定时任务demo2:2021-01-27T18:05:00.002,线程名称:scheduler-pool-0 线程id:28
定时任务demo1:2021-01-27T18:05:00.002,线程名称:scheduler-pool-2 线程id:51

注意:cron 为空, new CronTrigger(cron) 将抛出空指针异常

四、多线程定时任务

两种情况,如果是多个任务之间的多线程可以配置线程池解决,还有一种就是单任务多线程可以使用@Async解决,可以直接指定线程池@Async("taskExecutor")

1、单线程调度

SpringBoot 使用@scheduled定时执行任务的时候是在一个单线程中,如果有多个任务,其中一个任务执行时间过长,则有可能会导致其他后续任务被阻塞直到该任务执行完成。也就是会造成一些任务无法定时执行的错觉

可以通过如下代码进行测试:

    @Scheduled(cron = "0/1 * * * * ? ")
    public void task() throws InterruptedException {
        log.info("执行-task()," + "异步threadId:" + Thread.currentThread().getId());
        Thread.sleep(1000 * 5);
    }

    @Scheduled(cron = "0/1 * * * * ? ")
    public void job() {
        log.info("执行-job()," + "异步threadId:" + Thread.currentThread().getId());
    }

输出结果如下:

2021-03-29 10:37:02.002  INFO 13212 --- [pool-3-thread-1] : 执行-task(),异步threadId:31
2021-03-29 10:37:07.003  INFO 13212 --- [pool-3-thread-1] : 执行-job(),异步threadId:31
2021-03-29 10:37:08.001  INFO 13212 --- [pool-3-thread-1] : 执行-task(),异步threadId:31
2021-03-29 10:37:13.002  INFO 13212 --- [pool-3-thread-1] : 执行-job(),异步threadId:31
2021-03-29 10:37:14.001  INFO 13212 --- [pool-3-thread-1] : 执行-job(),异步threadId:31
2021-03-29 10:37:14.001  INFO 13212 --- [pool-3-thread-1] : 执行-task(),异步threadId:31

上面的日志中可以明显的看到job()被阻塞了,直达task()执行完后它才再次执行
日志信息中也可以看出@Scheduled是使用了一个线程池中的一个单线程来执行所有任务的。

2、解决办法

方法一:将@Scheduled注释的方法内部改成异步执行

更改如下:

	// 需要构建一个合理的线程池也是一个关键,否则提交的任务也会在自己构建的线程池中阻塞
    ExecutorService service = Executors.newFixedThreadPool(3);

    @Scheduled(cron = "0/1 * * * * ? ")
    public void task() {
        service.execute(() -> {
            log.info("执行-task()," + "异步threadId:" + Thread.currentThread().getId());
            //模拟长时间执行,比如IO操作,http请求
            try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }

    @Scheduled(cron = "0/1 * * * * ? ")
    public void job() {
        service.execute(() -> {
            log.info("执行-job()," + "异步threadId:" + Thread.currentThread().getId());
        });
    }

定义3个核心线程,打印结果如下:

2021-03-29 10:47:00.003  INFO 13760 --- [pool-3-thread-3] : 执行-job(),异步threadId:59
2021-03-29 10:47:00.003  INFO 13760 --- [pool-3-thread-3] : 执行-task(),异步threadId:59
2021-03-29 10:47:01.004  INFO 13760 --- [pool-3-thread-2] : 执行-job(),异步threadId:52
2021-03-29 10:47:01.004  INFO 13760 --- [pool-3-thread-2] : 执行-task(),异步threadId:52
2021-03-29 10:47:04.007  INFO 13760 --- [pool-3-thread-1] : 执行-job(),异步threadId:51
2021-03-29 10:47:04.007  INFO 13760 --- [pool-3-thread-1] : 执行-task(),异步threadId:51
2021-03-29 10:47:05.004  INFO 13760 --- [pool-3-thread-3] : 执行-job(),异步threadId:59
2021-03-29 10:47:05.004  INFO 13760 --- [pool-3-thread-3] : 执行-task(),异步threadId:59
2021-03-29 10:47:06.004  INFO 13760 --- [pool-3-thread-2] : 执行-job(),异步threadId:52
2021-03-29 10:47:06.004  INFO 13760 --- [pool-3-thread-2] : 执行-task(),异步threadId:52

以上日志信息可以看出,三个核心线程数还是阻塞了,构建一个合理的线程池很重要

方法二:Scheduled配置成成多线程执行

这种方式,两个任务之间不会冲突,但是跟单个任务执行时间有关,如下

import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;

@Configuration
@EnableScheduling
public class MySchedulingConfigurer implements SchedulingConfigurer {

    /**
     * 重写配置定时任务的方法.
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.setScheduler(taskScheduler());
    }

    @Bean(name = "taskExecutor")
    public ScheduledExecutorService taskScheduler() {
        //创建线程池
        return new ScheduledThreadPoolExecutor(5,
                new BasicThreadFactory.Builder().namingPattern("scheduler-pool-%d").daemon(true).build());
    }
}

打印结果如下:

2021-03-29 11:25:50.002  INFO 7944 --- [cheduler-pool-2] : 执行-job(),异步threadId:33
2021-03-29 11:25:50.002  INFO 7944 --- [cheduler-pool-4] : 执行-task(),异步threadId:59
2021-03-29 11:25:51.001  INFO 7944 --- [cheduler-pool-2] : 执行-job(),异步threadId:33
2021-03-29 11:25:52.001  INFO 7944 --- [cheduler-pool-2] : 执行-job(),异步threadId:33
2021-03-29 11:25:53.002  INFO 7944 --- [cheduler-pool-5] : 执行-job(),异步threadId:60
2021-03-29 11:25:54.003  INFO 7944 --- [cheduler-pool-3] : 执行-job(),异步threadId:56
2021-03-29 11:25:55.001  INFO 7944 --- [cheduler-pool-3] : 执行-job(),异步threadId:56
2021-03-29 11:25:56.001  INFO 7944 --- [cheduler-pool-5] : 执行-task(),异步threadId:60
2021-03-29 11:25:56.001  INFO 7944 --- [cheduler-pool-3] : 执行-job(),异步threadId:56
2021-03-29 11:25:57.002  INFO 7944 --- [cheduler-pool-3] : 执行-job(),异步threadId:56
2021-03-29 11:25:58.002  INFO 7944 --- [cheduler-pool-4] : 执行-job(),异步threadId:59

注意:以上日志可以看出这种方式,task()执行完成后才可以再次执行,有阻塞情况,但不会影响job()执行

根据方法一的方式改造:

    @Resource(name = "taskExecutor")
    private ScheduledExecutorService taskScheduler;

    @Scheduled(cron = "0/1 * * * * ? ")
    public void task() {
        taskScheduler.execute(() -> {
            log.info("执行-task()," + "异步threadId:" + Thread.currentThread().getId());
            //模拟长时间执行,比如IO操作,http请求
            try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }

    @Scheduled(cron = "0/1 * * * * ? ")
    public void job() {
        taskScheduler.execute(() -> {
            log.info("执行-job()," + "异步threadId:" + Thread.currentThread().getId());
        });
    }
2021-03-29 11:40:04.002  INFO 4852 --- [cheduler-pool-3] : 执行-task(),异步threadId:51
2021-03-29 11:40:05.016  INFO 4852 --- [cheduler-pool-2] : 执行-job(),异步threadId:33
2021-03-29 11:40:05.017  INFO 4852 --- [cheduler-pool-2] : 执行-task(),异步threadId:33
2021-03-29 11:40:06.004  INFO 4852 --- [cheduler-pool-4] : 执行-job(),异步threadId:52
2021-03-29 11:40:06.005  INFO 4852 --- [cheduler-pool-4] : 执行-task(),异步threadId:52
2021-03-29 11:40:07.003  INFO 4852 --- [cheduler-pool-1] : 执行-job(),异步threadId:32
2021-03-29 11:40:07.004  INFO 4852 --- [cheduler-pool-1] : 执行-task(),异步threadId:32
2021-03-29 11:40:08.001  INFO 4852 --- [cheduler-pool-5] : 执行-job(),异步threadId:53
2021-03-29 11:40:08.002  INFO 4852 --- [cheduler-pool-5] : 执行-task(),异步threadId:53
2021-03-29 11:40:09.003  INFO 4852 --- [cheduler-pool-3] : 执行-job(),异步threadId:51
2021-03-29 11:40:09.003  INFO 4852 --- [cheduler-pool-3] : 执行-task(),异步threadId:51
2021-03-29 11:40:10.017  INFO 4852 --- [cheduler-pool-2] : 执行-job(),异步threadId:33
2021-03-29 11:40:10.017  INFO 4852 --- [cheduler-pool-2] : 执行-task(),异步threadId:33

以上日志可以看出,方法一和方法二一起使用是才是正解,同样存在问题,需要合理化根据执行时间加大设置的核心线程数,或者自定义线程池将队列容量设置成0,以便线程数可以达到最大线程数

方法三:使用@Async注解

注意:使用时需要添加 @EnableAsync 开启异步 ------- 附:@Async注解的详细使用

    @Async
    @Scheduled(cron = "0/1 * * * * ? ")
    public void task() throws InterruptedException {
        log.info("执行-task()," + "异步threadId:" + Thread.currentThread().getId());
        Thread.sleep(1000 * 5);
    }

    @Async
    @Scheduled(cron = "0/1 * * * * ? ")
    public void job() {
        log.info("执行-job()," + "异步threadId:" + Thread.currentThread().getId());
    }

执行情况如下:

2021-03-29 14:08:35.007  INFO 1092 --- [cTaskExecutor-1] : 执行-task(),异步threadId:52
2021-03-29 14:08:35.007  INFO 1092 --- [cTaskExecutor-2] : 执行-job(),异步threadId:53
2021-03-29 14:08:36.001  INFO 1092 --- [cTaskExecutor-3] : 执行-job(),异步threadId:54
2021-03-29 14:08:36.002  INFO 1092 --- [cTaskExecutor-4] : 执行-task(),异步threadId:55
2021-03-29 14:08:37.000  INFO 1092 --- [cTaskExecutor-5] : 执行-job(),异步threadId:56
2021-03-29 14:08:37.002  INFO 1092 --- [cTaskExecutor-6] : 执行-task(),异步threadId:57
2021-03-29 14:08:38.001  INFO 1092 --- [cTaskExecutor-7] : 执行-task(),异步threadId:58
2021-03-29 14:08:38.001  INFO 1092 --- [cTaskExecutor-8] : 执行-job(),异步threadId:59
2021-03-29 14:08:39.002  INFO 1092 --- [cTaskExecutor-9] : 执行-task(),异步threadId:60
2021-03-29 14:08:39.002  INFO 1092 --- [TaskExecutor-10] : 执行-job(),异步threadId:61

查看日志,看起来没有问题,实现起来比较简单,其实存在问题,@Async默认异步配置使用的是SimpleAsyncTaskExecutor,该线程池默认来一个任务创建一个线程,在大量的任务堆积的时候,这时就会不断创建大量线程,极有可能压爆服务器内存造成OOM

以下 @Async 设置了 5个核心线程数,如何设置详见 @Async 注解的使用

/**
 * 基于注解设定多线程定时任务.
 */
@Component
@EnableScheduling   // 1.开启定时任务
@EnableAsync        // 2.开启多线程
public class MultithreadScheduleTask{
    @Async
    @Scheduled(fixedDelay = 1000)  //间隔1秒
    public void first() throws InterruptedException {
        System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() +
                ",线程 : " + Thread.currentThread().getName() + ",异步threadId:" + Thread.currentThread().getId());
        Thread.sleep(1000 * 10);
    }

    @Async
    @Scheduled(fixedDelay = 2000)
    public void second() {
        System.out.println("第二个定时任务开始 : " + LocalDateTime.now().toLocalTime() +
                ",线程 : " + Thread.currentThread().getName() + ",异步threadId:" + Thread.currentThread().getId());
    }
}

结果如下存在问题:@Scheduled 执行任务时有队列容量不会增加除核心线程数以外的线程,需要合理化根据执行时间加大核心线程数,采用自定义线程池并通过@Async(“asyncTaskExecutor”)指定该线程池,或者将队列容量设置成0,让它直接触发增大线程数以达到最大线程数

第一个定时任务开始 : 13:48:58.077,线程 : task-async-3,异步threadId:57
第二个定时任务开始 : 13:48:59.077,线程 : task-async-4,异步threadId:58
第一个定时任务开始 : 13:48:59.079,线程 : task-async-5,异步threadId:59
第一个定时任务开始 : 13:49:00.079,线程 : task-async-2,异步threadId:34
第二个定时任务开始 : 13:49:01.077,线程 : task-async-4,异步threadId:58
第一个定时任务开始 : 13:49:01.081,线程 : task-async-4,异步threadId:58
第一个定时任务开始 : 13:49:07.081,线程 : task-async-1,异步threadId:33
第二个定时任务开始 : 13:49:08.078,线程 : task-async-3,异步threadId:57
第一个定时任务开始 : 13:49:08.078,线程 : task-async-3,异步threadId:57
第一个定时任务开始 : 13:49:09.079,线程 : task-async-5,异步threadId:59
第二个定时任务开始 : 13:49:10.080,线程 : task-async-2,异步threadId:34

后记

springboot定时任务源码解析

推荐参考:https://blog.csdn.net/a842699897/article/details/83790282

Java定时任务

开发中,往往遇到另起线程执行其他代码的情况,用java定时任务接口ScheduledExecutorService来实现。

ScheduledExecutorService是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。

注意,只有当调度任务来的时候,ScheduledExecutorService才会真正启动一个线程,其余时间ScheduledExecutorService都是处于轮询任务的状态。

  // 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
  ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);

  // scheduleAtFixedRate() 每次执行时间为上一次任务开始起向后推一个时间间隔,是基于固定时间间隔进行任务调度
  scheduledExecutorService.scheduleAtFixedRate(() -> {
      System.out.println(Thread.currentThread().getName() + ": 定时执行任务!" + new Date());
  }, 5, 10, TimeUnit.SECONDS);

  // scheduleWithFixedDelay() 每次执行时间为上一次任务结束起向后推一个时间间隔,取决于每次任务执行的时间长短
  scheduledExecutorService.scheduleWithFixedDelay(() -> {
      System.out.println(Thread.currentThread().getName() + ": 定时执行任务!" + new Date());
  }, 5, 10, TimeUnit.SECONDS);

  // 只执行一次延时任务
  ScheduledExecutorService scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1,
          new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
  scheduledThreadPoolExecutor.schedule(() -> {
      System.out.println(Thread.currentThread().getName() + ": 定时执行任务!");
  }, 20, TimeUnit.SECONDS);

ScheduleAtFixedRate`每次执行时间为上一次任务开始起向后推一个时间间隔,即每次执行时间为`initialDelay,initialDelay+period,initialDelay+2*period。。。。。

ScheduleWithFixedDelay`每次执行时间为上一次任务结束起向后推一个时间间隔,即每次执行时间为:`initialDelay,initialDelay+executeTime+delay,initialDelay+2*executeTime+2*delay。。。。。

由此可见,ScheduleAtFixedRate是基于固定时间间隔进行任务调度,ScheduleWithFixedDelay取决于每次任务执行的时间长短,是基于不固定时间间隔进行任务调度。

  • 18
    点赞
  • 162
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值