Spring Boot 中使用 Spring Task 实现定时任务

工作 / 2022-03-31

Spring Boot 中使用 Spring Task 实现定时任务

前言

日常开发中经常使用定时任务。比如凌晨对于数据的结算。今天就来使用一下Spring Boot中使用Spring内置的定时任务。

使用SpringBoot创建定时任务非常简单,目前主要有以下三种创建方式:

  • 一、基于注解(@Scheduled)
  • 二、基于接口(SchedulingConfigurer) 前者相信大家都很熟悉,但是实际使用中我们往往想从数据库中读取指定时间来动态执行定时任务,这时候基于接口的定时任务就派上用场了。
  • 三、基于注解设定多线程定时任务

一、静态:基于注解

1.开启定时任务

Spring Boot 默认在无任何第三方依赖的情况下使用 spring-context 模块下提供的定时任务工具 Spring Task。我们只需要使用 @EnableScheduling 注解就可以开启相关的定时任务功能。

package cn.zwy.demo;

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

/**
 * @author zwy.中国
 */
@SpringBootApplication
@EnableScheduling
public class xxxApplication {

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

}

如上就可以通过注解的方式实现自定义定时任务,下面将详细的介绍定时任务。

1.1 使用**@Scheduled** 注解实现定时任务
package cn.zwy.demo;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @author zwy.中国
 **/
@Component
public class TaskService {
    //添加定时任务 每5秒使用
    @Scheduled(cron = "0/5 * * * * ?")
    public void task() {
        log.info("执行定时任务")
    }
}

请注意:@Scheduled 注解中一定要声明定时任务的执行策略 cronfixedDelayfixedRate 三选一。

1.2 cron

cron 使用石英表达式来配置定时任务,在某个时间运行。

Cron https://zwy.xn--fiqs8s/archives/cronhttpswwwmatoolscomcron

1.3 fixedDelay

fixedDelay。 相当于一条 核酸队伍,每个人都得等待上一个人做完(任务结束),护士姐姐用酒精消完毒(固定的等待时长),才会去进行下一个人的消毒(下一轮的任务)。 它的间隔时间 是从它的上一次任务结束的时候开始计时,跟任务逻辑的执行时间无关,两次轮询得间隔是一样的。

fixedDelay

package cn.zwy.demo;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @author zwy.中国
 **/
@Component
public class TaskService {
    //上一个任务结束后1s后再执行
    @Scheduled(fixedDelay  = "1000")
    public void task() {
        log.info("执行定时任务")
    }
}
1.4 fixedRate

fixedRate。不太好比喻。

大致用示意字符串来表示如下(每个 T1, 或 T2 代表任务执行秒数(每次任务执行时间不定),假定 fixedRate 或 fixedDelay 的值是 5 秒,用 W 表示等待的数)

fixedRate: T1.T1WWWT2.T2.T2WW.T3.T3.T3.T3.T3.T4.T4.T4.T4.T4.T4.T4T5T5WWWT6.T6........

fixedDelay: T1.T1.WWWWW.T2.T2.T2WWWWW.T3.T3.T3.T3.T3.WWWWW.T4.T4.T4.T4.T4.T4.T4.WWWWWT6.T6......

fixedRate 相当于一个预期定时,在预期内完成皆大欢喜,超过了预期就必须阻塞下一个任务,一旦上一个任务结束,下一轮立刻执行。

fixedRate

package cn.zwy.demo;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @author zwy.中国
 **/
@Component
public class TaskService {
    //每个任务的理想状态为1s内完成
    @Scheduled(fixedRate  = "1000")
    public void task() {
        log.info("执行定时任务")
    }
}
1.5 initialDelay

initialDelay。 初始化延迟时间,也就是第一次延迟执行的时间。这个参数对 cron 属性无效,只能配合 fixedDelayfixedRate 使用。如 @Scheduled(initialDelay=5000,fixedDelay = 1000) 表示第一次延迟 5000 毫秒执行,下一次任务在上一次任务结束后 1000 毫秒后执行。

二、基于接口(SchedulingConfigurer)

使用这个方式相当于更新数据库后每次执行任务都重新读取新的cron来重新配置下一次的执行时间,所以在定时任务执行之后,必须保证下次定时任务执行之前读取的cron必须是正确的才能保证定时任务下次能正常运行。

package cn.zwy.demo;

import com.alibaba.druid.util.StringUtils;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.beans.factory.annotation.Autowired;
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.time.LocalDateTime;

/**
 * @Description: 接口定时任务
 * @author: zwy
 * @date: 2022年03月31日 17:51
 */
@Configuration      //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling   // 2.开启定时任务
public class DynamicScheduleTask implements SchedulingConfigurer {
    @Mapper
    public interface CronMapper {
       //保证数据库有这张表 返回的 cron字段就是 cron表达式 必须完整且正确
        @Select("select cron from cron limit 1")
        public String getCron();
    }
    @Autowired      //注入mapper
    @SuppressWarnings("all")
    CronMapper cronMapper;
    /**
     * 执行定时任务.
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(
                //1.添加任务内容(Runnable)
                () -> System.out.println("执行动态定时任务: " + LocalDateTime.now().toLocalTime()),
                //2.设置执行周期(Trigger)
                triggerContext -> {
                    //2.1 从数据库获取执行周期
                    String cron = cronMapper.getCron();
                    //2.2 合法性校验.
                    if (StringUtils.isEmpty(cron)) {
                        // Omitted Code ..
                    }
                    //2.3 返回执行周期(Date)
                    return new CronTrigger(cron).nextExecutionTime(triggerContext);
                }
        );
    }
}

三、基于注解设定多线程定时任务

package cn.heypad.motorman.module.redis;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

//@Component注解用于对那些比较中立的类进行注释;
//相对与在持久层、业务层和控制层分别采用 @Repository、@Service 和 @Controller 对分层中的类进行注释
@Component
@EnableScheduling   // 1.开启定时任务
@EnableAsync        // 2.开启多线程
public class MultithreadScheduleTask {

        @Async
        @Scheduled(fixedDelay = 1000)  //间隔1秒
        public void first() throws InterruptedException {
            System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
            System.out.println();
            Thread.sleep(1000 * 10);
        }

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

线程的关键就是 @Async 注解,但是由于开启了线程第一个任务执行的时间也不受本身的执行时间影响,所以很可能会出现重复操作导致数据异常,所以推荐尽量明确业务的时候在进行使用。