zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Spring boot实现定时任务二:使用注解@scheduled和@EnableScheduling

SpringBoot 实现 任务 注解 定时 Scheduled 使用
2023-09-11 14:20:19 时间

1.定时任务的使用场景

在项目的开发过程中,我们经常会遇到类似这样的需求需要定期执行某一项工作,或者按照特定计划执行某些工作。这时我们就需要用到定时任务

使用SpringBoot创建定时任务非常简单,目前主要有以下三种创建方式:
(1)基于注解(@Scheduled): 如果定时任务执行时间较短,并且比较单一,可以使用这个注解
(2)基于接口(SchedulingConfigurer)前者相信大家都很熟悉,但是实际使用中我们往往想从数据库中读取指定时间来动态执行定时任务,这时候基于接口的定时任务就派上用场了。
(3)开源的第三方框架: Quartz 或者 elastic-job

2.SpringBoot对定时任务的支持:

日常开发中,定时任务最常用的实现方式有如下两种:

  • Spring-3.*版本之后,自带定时任务的实现(@Scheduled注解)
  • SpringBoot-2.*版本之后,均实现了Quartz的自动配置

3.Spring自带定时任务的实现—@Scheduled注解

Spring自带定时任务的实现,是靠@Scheduled注解实现的。在SpringBoot项目中,我们想使该注解生效,还需要在类上加注解@EnableScheduling,开启对Spring定时任务的支持,测试源码如下:

4.代码实现

步骤流程如下:
(1)在spring boot的启动类上面添加 @EnableScheduling 注解
(2)创建一个类,在类前加@Configuration注解。在类中需要定时执行的方法上方加上@Scheduled注解
(3)然后为Scheduled注解加上属性,启动项目,就可以了。

4.1创建springboot项目,添加web依赖

<!--web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

4.2 添加一个类,配置定时任务

创建一个类,在类前加@Configuration注解。在类中需要定时执行的方法上方加上@Scheduled注解,示例如下:

package com.example.scheduleddemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 定时任务
 * @author qzz
 */
@Configuration
public class ScheduleTask {

    /**
     * 添加定时任务:每隔5秒执行一次
     */
    @Scheduled(cron = "0/5 * * * * ?")
    private void configureTask(){

        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("执行静态定时任务时间:"+df.format(new Date()));
    }
}

4.3 启动类上添加注解@EnableScheduling

package com.example.scheduleddemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
 * 启动类
 * @author qzz
 */
@SpringBootApplication
@EnableScheduling
public class ScheduledDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(ScheduledDemoApplication.class, args);
    }

}

4.3 启动项目,查看控制台输出

请添加图片描述
@Scheduled中的属性cron代表cron表达式,它是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,具体使用方法请自行百度。这里放出部分用法示例。除了cron属性,@Scheduled还有fixedDelay,fixedRate等属性可以使用

// Cron表达式范例:
每隔5秒执行一次:*/5 * * * * ?
每隔1分钟执行一次:0 */1 * * * ?
每天23点执行一次:0 0 23 * * ?
每天凌晨1点执行一次:0 0 1 * * ?
每月1号凌晨1点执行一次:0 0 1 1 * ?
每月最后一天23点执行一次:0 0 23 L * ?
每周星期天凌晨1点实行一次:0 0 1 ? * L26分、29分、33分执行一次:0 26,29,33 * * * ?
每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?

5.Scheduled属性的用法

5.1 fixedRate

fixedRate表示上一个执行开始后再一次执行的时间,但是必须要等上一次执行完成后才能执行。如果上一次执行完成了,还没到下一次执行的时间 ,就会等到下一次执行的时间到了才会执行,如果下一次的执行时间到了,上一次还没执行完,那么就会等到 上一次执行完成才会执行下一次。=后面的值 1000 为1秒。
(1)每秒执行一次

    /**
     * 添加定时任务:每秒执行一次
     */
    @Scheduled(fixedRate = 1000)
    private void fixedRate() throws Exception {
        System.out.println("fixedRate开始执行时间:"+new Date(System.currentTimeMillis()));
    }

控制台信息:
在这里插入图片描述
(2)每秒执行一次,休眠8秒

    /**
     * 添加定时任务:每秒执行一次,休眠8秒
     */
    @Scheduled(fixedRate = 1000)
    private void fixedRate() throws Exception {
        System.out.println("fixedRate开始执行时间:"+new Date(System.currentTimeMillis()));
        //休眠8秒
        Thread.sleep(1000*8);
        System.out.println("fixedRate执行结束时间:"+new Date(System.currentTimeMillis()));
    }

我们启动项目看看这个定时任务的执行情况,可以看到开始和结束时间之间间隔了8秒,然后马上执行了下一次:

在这里插入图片描述

5.2 fixedDelay

fixedDelay表示上一次执行结束后再一次执行的时间,启动项目,可以看到上一次执行结束后还等了1秒才执行下一次。

    /**
     * 添加定时任务:fixedDelay表示上一次执行结束后再一次执行的时间
     */
    @Scheduled(fixedDelay = 1000)
    private void fixedDelay() throws Exception {
        System.out.println("fixedDelay开始执行时间:"+new Date(System.currentTimeMillis()));
        //休眠2秒
        Thread.sleep(1000*2);
        System.out.println("fixedDelay执行结束时间:"+new Date(System.currentTimeMillis()));
    }

在这里插入图片描述

5.3 initialDelay

initialDelay表示项目启动后延迟多久执行定时任务,所以他要配合fixedRate或fixedDelay一起使用

    /**
     * 添加定时任务:initialDelay表示项目启动后延迟多久执行定时任务,所以他要配合fixedRate或fixedDelay一起使用
     */
    @Scheduled(initialDelay = 1000*3,fixedDelay = 1000)
    private void fixedDelay() throws Exception {
        System.out.println("initialDelay开始执行时间:"+new Date(System.currentTimeMillis()));
    }

在这里插入图片描述

5.4 cron

cron是一种表达式,具体的写法规格有兴趣的可以去学习一下,不懂也不影响使用,直接按照示例就可以类推,写出自己想要的,也可以直接使用在线生成。

在这里插入图片描述

示例:

    /**
     * 添加定时任务:每隔5秒执行一次   cron
     */
    @Scheduled(cron = "0/5 * * * * ?")
    private void configureTask(){

        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("执行静态定时任务时间:"+df.format(new Date()));
    }

启动项目后,可以看到每次的执行时间间隔了5秒
在这里插入图片描述

6.使用@scheduled定时执行任务需要注意的坑

6.1要注意什么坑

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

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

package com.example.scheduleddemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 定时任务
 * @author qzz
 */
@Configuration
public class ScheduleTask2 {

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

    /**
     * 添加定时任务:每隔1秒执行一次   cron
     */
    @Scheduled(cron = "0/1 * * * * ?")
    private void deleteFile() throws Exception {

        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("111 delete success,time:"+df.format(new Date()));
        //模拟长时间执行,比如IO操作,http请求
        Thread.sleep(1000*5);
    }

    /**
     * 添加定时任务:每隔1秒执行一次   cron
     */
    @Scheduled(cron = "0/1 * * * * ?")
    private void syncFile() throws Exception {

        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("222 sync success,time:"+df.format(new Date()));
    }
}

输出如下:
在这里插入图片描述
上面的日志中可以明显的看到syncFile被阻塞了,直达deleteFile执行完它才执行了。而且从日志信息中也可以看出@Scheduled是使用了一个线程池中的一个单线程来执行所有任务的

如果把Thread.sleep(1000*5)注释了,输出如下:
在这里插入图片描述
这下正常了

6.2 解决办法

方法1:使用 @Async 注解采用异步执行方式

在Spring中,基于@Async标注的方法,称之为异步方法,这些方法将在执行的时候,将会在独立的线程中被执行,调用者无需等待它的完成,即可继续其他的操作。

如下:

启动类添加@EnableScheduling开启定时任务,添加@EnableAsync开启异步支持

package com.example.scheduleddemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
 * 启动类
 * @author qzz
 */
@SpringBootApplication
@EnableScheduling
@EnableAsync
public class ScheduledDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(ScheduledDemoApplication.class, args);
    }

}

创建定时任务实例:

package com.example.scheduleddemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 单线程任务阻塞的解决方法:
 *    (1)使用 @Async 注解采用异步执行方式 ,定时任务实例上添加@Async和@Scheduled注解, 启动类添加@EnableScheduling开启定时任务,添加@EnableAsync开启异步支持
 * @author qzz
 */
@Configuration
public class ScheduleTask3 {

    /**
     * 添加定时任务:每隔1秒执行一次   cron
     */
    @Async
    @Scheduled(cron = "0/1 * * * * ?")
    public void deleteFile() throws Exception {

        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("第一个定时任务开始:"+df.format(new Date()));
        //模拟长时间执行,比如IO操作,http请求
        Thread.sleep(1000*5);
        System.out.println("第一个定时任务结束:" + df.format(new Date()));
    }

    /**
     * 添加定时任务:每隔1秒执行一次   cron
     */
    @Async
    @Scheduled(cron = "0/1 * * * * ?")
    public void syncFile() throws Exception {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("第二个定时任务开始:"+df.format(new Date()));
        //模拟长时间执行,比如IO操作,http请求
        Thread.sleep(1000*5);
        System.out.println("第二个定时任务结束:" + df.format(new Date()));
    }
}

执行结果:

在这里插入图片描述Async 在未指定线程池时,使用的是springBoot内置的线程池
那如何指定使用自定义的线程池呢?
下面是配置Async异步执行使用自定义线程池的步骤。

1)自定义线程池

package com.example.scheduleddemo.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

/**
 * 自定义线程池
 * @author qzz
 */
@Configuration
@EnableAsync
public class ExecutorConfig {

    private static final Logger logger = LoggerFactory.getLogger(ExecutorConfig.class);

    @Bean(name = "myThreadPool")
    public Executor myThreadPool(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //表示线程池核心线程,正常情况下开启的线程数量
        executor.setCorePoolSize(30);
        //配置队列大小
        executor.setQueueCapacity(500);
        //当核心线程都在跑任务,还有多余的任务会存在此处
        executor.setMaxPoolSize(50);
        //非核心线程的超时时长,超长后会被回收
        executor.setKeepAliveSeconds(60);
        //配置线程池前缀
        executor.setThreadNamePrefix("MyThreadPool-");
        //用来设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //配置线程池拒绝策略
        executor.setRejectedExecutionHandler((Runnable r, ThreadPoolExecutor exe)->{
            logger.warn("MyThreadPool-当前任务线程池队列已满");
        });
        //初始化线程池
        executor.initialize();

        return executor;
    }
}

2)创建定时任务示例,只需在Async注解后指定线程池名即可

    /**
     * 添加定时任务:每隔1秒执行一次   cron    @Async使用自定义线程池
     */
    @Async("myThreadPool")
    @Scheduled(cron = "0/1 * * * * ?")
    public void test() throws Exception {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("第三个定时任务开始:"+df.format(new Date()));
        //模拟长时间执行,比如IO操作,http请求
        Thread.sleep(1000*3);
        System.out.println("第三个定时任务结束:" + df.format(new Date()));
    }

方法2:使用并行方式
当定时任务很多的时候,为了提高任务执行效率,可以采用并行方式执行定时任务,任务之间互不影响, 只要实现SchedulingConfigurer接口就可以

/**
    定时任务并行执行
**/
@EnableScheduling
@Configuration
public class ScheduledConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
    //这里设置的线程池是corePoolSize也是很关键了,自己根据业务需求设定
    scheduledTaskRegistrar.setScheduler(setTaskExecutors());
}

@Bean(destroyMethod="shutdown")
public Executor setTaskExecutors(){
    return Executors.newScheduledThreadPool(3); // 3个线程来处理。
}}

spring boot 用@Scheduled注解实现定时任务就介绍到这里。

示例源码可点击此处进行下载

参考资料:
SpringBoot使用Schedule实现异步执行定时任务

springboot 基于@Scheduled注解 实现定时任务