Spring Schedule

849 阅读6分钟

简介

背景

在项目开发过程中,我们经常需要执行具有周期性的任务。通过定时任务可以很好的帮助我们实现。

我们拿常用的几种定时任务框架做一个比较:

定时任务框架 Cron表达式 固定间隔执行 固定频率执行 任务持久化 开发难易度
JDK TimerTask 不支持 支持 支持 不支持 一般
Spring Schedule 支持 支持 支持 不支持 简单
Quartz 支持 支持 支持 支持 困难

优点

基于注解来设置调度器。
非常方便实现简单的调度。
对代码不具有入侵性,非常轻量级。
所以我们会发现,spring schedule 用起来很简单,非常轻量级, 对代码无侵入性, 我们只需要注重业务的编写, 不需要关心如果构造Scheduler。

缺点

一旦调度任务被创建出来, 不能动态更改任务执行周期, 对于复杂的任务调度有一定的局限性。

使用说明

    @Component
    public class Demo{
    
        @Scheduled(fixedRate = 1000)
        public void do(){
            doSomething();
        }
    }
以上是1秒执行一次。

注解详解

spring schedule的核心就是Scheduled注解的使用
    public @interface Scheduled {
        String cron() default ""; // 使用cron表达式
        String zone() default "";
        long fixedDelay() default -1L; //每次执行任务之后间隔多久再次执行该任务。
        String fixedDelayString() default "";
        long fixedRate() default -1L; // 执行频率,每隔多少时间就启动任务,不管该任务是否启动完成
        String fixedRateString() default "";
        long initialDelay() default -1L;  //初次执行任务之前需要等待的时间
        String initialDelayString() default "";
    }

SpringBoot集成schedule

1、添加maven依赖包

由于Spring Schedule包含在spring-boot-starter基础模块中,所有不需要增加额外的依赖
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

2、启动类,添加启动注解

在springboot入口或者配置类中增加@EnableScheduling注解即可启用定时任务。
    @EnableScheduling
    @SpringBootApplication
    public class ScheduleApplication {
        public static void main(String[] args) {
            SpringApplication.run(ScheduleApplication.class, args);
        }
    }

添加定时任务

我们将对Spring Schedule三种任务调度器分别举例说明。

1.3.1Cron表达式

类似于Linux下的Cron表达式时间定义规则。Cron表达式由6或7个空格分隔的时间字段组成,如下图:
位置 时间域名 允许值 允许的特殊字符
1 0-59 ,-*/
2 分钟 0-59 ,-*/
3 小时 0-23 ,-*/
4 日期 1-31 ,-*/L W C
5 月份 1-12 ,-*/
6 星期 1-7 ,-*/L C #
7 年(可选) 空值 1970-2099 ,-*/

常用表达式:

表达式 描述
0/30 * * * * * 每30秒执行一次
0 0/5 * * * * 每5分钟执行一次
0 0 0 * * * 每天凌晨执行
0 0 8, 12, 17 * * * 每天8点、12点、17点整执行
0 30 3-5 * * * 每天3点~5点 30分时执行
举个栗子:
添加一个work()方法,每10秒执行一次。

注意:当方法的执行时间超过任务调度频率时,调度器会在下个周期执行。

如:假设work()方法在第0秒开始执行,方法执行了12秒,那么下一次执行work()方法的时间是第20秒。
    @Component
    public class MyTask {
        @Scheduled(cron = "0/10 * * * * *")
        public void work() {
            // task execution logic
        }
    }

1.3.2 固定间隔任务

下一次的任务执行时间,是从方法最后一次任务执行结束时间开始计算。并以此规则开始周期性的执行任务。

举个栗子:

添加一个work()方法,每隔10秒执行一次。

例如:假设work()方法在第0秒开始执行,方法执行了12秒,那么下一次执行work()方法的时间是第22秒。
    @Scheduled(fixedDelay = 1000*10)
    public void work() {
        // task execution logic
    }

1.3.3 固定频率任务

按照指定频率执行任务,并以此规则开始周期性的执行调度。

举个栗子:

添加一个work()方法,每10秒执行一次。

注意:当方法的执行时间超过任务调度频率时,调度器会在当前方法执行完成后立即执行下次任务。

例如:假设work()方法在第0秒开始执行,方法执行了12秒,那么下一次执行work()方法的时间是第12秒。
    @Scheduled(fixedRate = 1000*10)
    public void work() {
        // task execution logic
    }

配置TaskScheduler线程池

在实际项目中,我们一个系统可能会定义多个定时任务。那么多个定时任务之间是可以相互独立且可以并行执行的。

通过查看org.springframework.scheduling.config.ScheduledTaskRegistrar源代码,发现spring默认会创建一个单线程池。这样对于我们的多任务调度可能会是致命的,当多个任务并发(或需要在同一时间)执行时,任务调度器就会出现时间漂移,任务执行时间将不确定。
    protected void scheduleTasks() {
        if (this.taskScheduler == null) {
            this.localExecutor = Executors.newSingleThreadScheduledExecutor();
            this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
        }
        //省略...
    }

自定义线程池

新增一个配置类,实现SchedulingConfigurer接口。重写configureTasks方法,通过taskRegistrar设置自定义线程池。
    @Configuration
    public class ScheduleConfig implements SchedulingConfigurer {
        @Override
	    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
	        taskRegistrar.setTaskScheduler(taskScheduler());
	    }

	    @Bean()
	    public TaskScheduler taskScheduler() {
	    	/**
         	* 使用优先级队列DelayedWorkQueue,保证添加到队列中的任务,会按照任务的延时时间进行排序,延时时间少的任务首先被获取。
         	* 资料https://www.jianshu.com/p/587901245c95
         	*/
	        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
	        // 配置核心线程池大小,根据任务数量定制,默认的线程池最大能创建的线程数目大小是Integer.MAX_VALUE
	        taskScheduler.setPoolSize(20);
	        // 线程名称前缀
	        taskScheduler.setThreadNamePrefix("xx-task-scheduler-thread-");
	        // 线程池关闭前最大等待时间,确保最后一定关闭
	        taskScheduler.setAwaitTerminationSeconds(30);
	        // 线程池关闭时等待所有任务完成
	        taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
	        // 任务丢弃策略 ThreadPoolExecutor.AbortPolicy()丢弃任务并抛出RejectedExecutionException异常。
	        taskScheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
	        return taskScheduler;
	    }
    }

使用SpringBoot的@Async来执行多线程并行处理

@Async配置

    /**
     * 线程配置
     * @author yc
     */
    
    @Configuration
    public class ThreadConfig implements AsyncConfigurer {
        /**
         * The {@link Executor} instance to be used when processing async
         * method invocations.
         * 默认策略为ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
         * 默认keepAliveTime时间为60秒
         * corePoolSize默认为1
         * maxPoolSize 默认为 Integer.MAX_VALUE;
         * keepAliveSeconds 默认为 60秒(单位为TimeUnit.SECONDS)
         * queueCapacity 默认为 Integer.MAX_VALUE;
         * 当队列参数为整数为LinkedBlockingQueue
         * 其他队列参数为SynchronousQueue
         *
         */
        @Override
        public Executor getAsyncExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(20);
            executor.setMaxPoolSize(40);
            executor.setQueueCapacity(100);
            executor.initialize();
            return executor;
        }
        /**
         * The {@link AsyncUncaughtExceptionHandler} instance to be used
         * when an exception is thrown during an asynchronous method execution
         * with {@code void} return type.
         */
        @Override
        public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
            return null;
        }
    }

定时任务使用

    @Component
    @Slf4j
    @Async
    public class ScheduleTask {
    
        @Autowired
        private XXXTask xxxTask;
    
        /**
         * xxx定时任务
         */
        @Scheduled(cron = "0 */2 * * * ?")
        public void xxxTask(){
            log.info("定时任务xxxTask开始时间:"+ DateUtils.currentTime());
            try{
                xxxTask.xxx();
            }catch (WalletException e){
                log.error("--------------------------------发生时间{},异常是{}",
                        DateUtils.currentTime(),e.getResultVoError().getMsg());
            }
            log.info("定时任务xxxTask结束时间:"+ DateUtils.currentTime());
        }
    }

参考网站

www.cnblogs.com/skychenjiaj… www.jianshu.com/p/587901245… www.cnblogs.com/dolphin0520…