SpringBoot学习小结之定时任务

一、Spring自带定时任务

Spring自带定时任务相关类位于spring-context包

1.1 注解

  • @Scheduled

    标记方法定时执行。所标记的方法必须没有参数,返回值会被忽视。以下属性必须满足一个:

    • cron 支持cron表达式,不支持year字段
    • fixedDelay 上次调用结束和下次调用开始间隔时间,单位毫秒
    • fixedDelayString 支持毫秒字符串、占位符、符合java.time.Duration解析的字符串
    • fixedRate 上次调用开始和下次调用开始间隔时间,单位毫秒
    • fixedRateStringfixedDelayString格式类似

    还有两个延迟属性

    • initialDelay 初始延迟毫秒数,默认-1
    • initialDelayStringfixedDelayString格式类似
  • @EnableScheduling

    开启任务调度,可以和@SpringBootApplication一起使用,或者和@Configuration一起,会确保配置类中或者使用@ComponentScan所扫描到的带有@Scheduled注解的Bean任务执行。

    默认情况下,会在配置类中优先搜索org.springframework.scheduling.TaskScheduler类型或名字为taskScheduler的Bean,如果没找到会搜索java.util.concurrent.ScheduledExecutorService类型的Bean,如果二者都没找到,会默认通过ConcurrentTaskScheduler创建一个单线程的ScheduledExecutorService ( SpringBoot 2.1.0 后会默认创建ThreadPoolTaskExecutor不再是单线程)

     Executors.newSingleThreadScheduledExecutor()
    
  • @Async

    标记目标方法异步执行,标记类代表所有方法异步执行,不能标记带有@Configuration的类。

    目标方法返回值被限定为voidjava.util.concurrent.Future

    • value 值是类型为 java.util.concurrent.Executororg.springframework.core.task.TaskExecutor的Bean名字,存在多个实例,可以指定Bean名字执行方法
  • @EnableAsync

    开启异步方法执行,和@EnableScheduling类似,会搜索org.springframework.core.task.TaskExecutor,或名字叫taskExecutorjava.util.concurrent.Executor类型的Bean,两个都没找到会默认创建org.springframework.core.task.SimpleAsyncTaskExecutor

    返回值为void的方法,异常不能被捕获,为了解决这个问题,可以通过实现AsyncConfigurer接口自定义配置。

1.2 配置

springboot 2.1.0之后自带任务调度器,可以通过以下参数进行配置,也可以自己使用@Configuration自定义不同的任务调度器

 spring:
   task:
     execution:
       pool:
         allow-core-thread-timeout: true # 是否允许核心线程超时
         core-size: 8 # 核心线程数量
         keep-alive: 60s #存活时间
         max-size: Integer.MAX_VALUE # 最大线程数量
         queue-capacity: Integer.MAX_VALUE #队列容量

1.3 主要类

  • ScheduledAnnotationBeanPostProcessor

    主要是通过这个类来处理定时任务,这个类是Bean的生命周期接口BeanPostProcessor的实现类。

    • postProcessAfterInitialization

      通过这个方法查找Bean中@Scheduled@Schedules标记的类和方法

      public Object postProcessAfterInitialization(Object bean, String beanName) {
        	if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
        				bean instanceof ScheduledExecutorService) {
        			// Ignore AOP infrastructure such as scoped proxies.
        		return bean;
        	}
        
        	Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
        	if (!this.nonAnnotatedClasses.contains(targetClass) &&
        				AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {
        			Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
        					(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
        						Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
        								method, Scheduled.class, Schedules.class);
        						return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
        					});
        			if (annotatedMethods.isEmpty()) {
        				this.nonAnnotatedClasses.add(targetClass);	
        			}
        			else {
        				annotatedMethods.forEach((method, scheduledMethods) ->
        						scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
        		}
        	}
        	return bean;
      }
      
    • processScheduled

      通过此方法来解析定时任务方法,三种注解所带来不同的定时任务

  • ScheduledTaskRegistrar

    定时任务注册器,可通过此类在配置类中添加定时任务。

1.4 动态定时任务

定时任务执行的cron表达式存放在数据库中,可动态修改动态执行

DROP TABLE IF EXISTS `cron`;
CREATE TABLE `cron` (
    `id` int(11) NOT NULL,
    `cron` varchar(255) NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
spring:
  datasource:
    username: 
    password: 
    url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useSSL=false&useUnicode=true
    driver-class-name: com.mysql.jdbc.Driver
  @Mapper
  @Repository
  public interface CronMapper {
      @Select("select cron from cron limit 1")
      String getCron();
  }
 public class OneTask implements Runnable{
     private static final Logger logger = LoggerFactory.getLogger(OneTask.class);
 
     @Override
     public void run() {
         logger.info("->执行");
     }
 }
 @Configuration
 @EnableScheduling
 public class MyScheduleConfig implements SchedulingConfigurer {
 
     private static final Logger logger = LoggerFactory.getLogger(MyScheduleConfig.class);
 
     @Autowired
     private CronMapper mapper;
     private String preCron;
     @Override
     public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
 
         scheduledTaskRegistrar.addTriggerTask(new OneTask(), triggerContext -> {
             String cron = mapper.getCron();
             // 校验cron表达式是否合法
             if (!CronSequenceGenerator.isValidExpression(cron)) {
                 logger.info("数据库cron表达式非法:{}", cron);
                 return new CronTrigger( preCron).nextExecutionTime(triggerContext);
             }
              preCron = cron;
             return new CronTrigger(cron).nextExecutionTime(triggerContext);
         });
     }
 }

二、Quartz

2.1 主要接口

  • Scheduler

    Quartz实现任务调用最主要的接口,它包含JobDetailTrigger的注册和调度器的执行

  • SchedulerFactory

    提供获取Scheduler的工厂接口,一般使用quartz提供实现类StdSchedulerFactory

  • Trigger

    触发器接口,和Job执行相关,包含触发开始时间,结束时间, 下次开始时间等信息

    • SimpleTrigger

      给定时间间隔触发,还可以指定次数

    • CronTrigger

      cron表达式触发

    • CalendarIntervalTrigger

      SimpleTrigger类似,间隔时间触发,不同的是SimpleTrigger间隔时间是毫秒,不能指定每个月(毫秒不固定)触发,CalendarIntervalTrigger可以根据日历单位为时间间隔触发

    • DailyTimeIntervalTrigger

      可以指定每天的某个时间段内,以一定的时间间隔执行任务,支持指定星期

  • JobDetail

    ​ 描述Job实例详细属性的接口

  • Job

    ​ 定时任务所要实现的接口

2.2 主要builder

  • ScheduleBuilder

    • SimpleScheduleBuilder

      // 每隔1s执行,重复10次
      SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                      .withIntervalInSeconds(1)
                      .withRepeatCount(10);
      
    • CronScheduleBuilder

      // 每分钟0 10 .. 50秒执行
      CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?");
      
    • CalendarIntervalScheduleBuilder

      // 每隔1个月执行1次
      CalendarIntervalScheduleBuilder cbuilder = CalendarIntervalScheduleBuilder.calendarIntervalSchedule()
                      .withIntervalInMonths(1);
      
    • DailyTimeIntervalScheduleBuilder

      // 每天9点半到18点半,每隔30秒执行一次
      DailyTimeIntervalScheduleBuilder dailyBuilder = DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
                      .withIntervalInSeconds(30)
                      .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 30))
                      .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 30));
      
  • JobBuilder

    JobDetail jobDetail = JobBuilder.newJob(CustomJob.class).withIdentity("job1", "group1").build();
    
  • TriggerBuilder

    Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity("trigger1")
        			.startNow()
                    .withSchedule(simpleScheduleBuilder).build();
    

    实际执行次数=重复次数+1,重复次数和结束时间冲突看谁先结束

  • 示例

    public class CustomJob implements Job {
        private static final Logger logger = LoggerFactory.getLogger(CustomJob.class);
    
        @Override
        public void execute(JobExecutionContext context) throws JobExecutionException {
            logger.info("-> invoke");
        }
    }
    
    SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = factory.getScheduler();
    		
    SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInSeconds(10)
                    .withRepeatCount(1);
    
    CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?");
    
    CalendarIntervalScheduleBuilder cbuilder = CalendarIntervalScheduleBuilder.calendarIntervalSchedule()
                    .withIntervalInMonths(1);
    
    DailyTimeIntervalScheduleBuilder dailyBuilder = DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
                    .withIntervalInSeconds(30)
                    .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 30))
                    .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 30));
    
    LocalDateTime now = LocalDateTime.now().plusMinutes(1);
    ZonedDateTime zonedDateTime = now.atZone(ZoneId.systemDefault());
    Instant instant = zonedDateTime.toInstant();
    Date newDate = Date.from(instant);
    
    JobDetail jobDetail = JobBuilder.newJob(CustomJob.class).withIdentity("job1", "group1").build();
    Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity("trigger1")
                    .startNow()
                    .endAt(newDate)
                    .withSchedule(simpleScheduleBuilder).build();
    
    scheduler.scheduleJob(jobDetail, trigger);
    scheduler.start();
    

2.3 cron表达式

cron表达式是由六个或七个子表达式(字段)组成的字符串,用于描述计划的各个详细信息

秒 分 时 日 月 周 年

字段名字 是否必填 允许值 允许特殊字符
Y 0-59 , - * /
Y 0-59 , - * /
Y 0-23 , - * /
Y 1-31 , - * / ? L W C
Y 0-11 或 JAN-DEC , - * /
Y 1-7 或 SUN-SAT , - * / ? L C #
N 空或 1970-2099 , - * /

特殊字符说明

  • *: 代表所有,每个值。例如 *在秒字段,代表每秒
  • ?: 代表无具体值。常用于周和日上,选定某日,不在乎是否周几或者选定周几,不在乎某日。
  • - 用于指定范围。例如1-3 在小时字段代表1,2,3小时
  • , 用于指定一些值。例如1,2,4 在小时字段代表1,2,4小时
  • / 用于指定增量。例如 0/15 在分钟字段代表第0,15,30,45分钟,5/15代表第5,20,35,50分钟
  • L 代表最后一个,在日和周两个字段中,单独使用L,在日中代表月的最后一日,在周中代表周六。如果加上数字,例如6L在周字段上,代表月最后一个星期五。也可以用L-3在日字段上表示月的倒数第三天。
  • W 用于指定最接近的工作日(周一到周五),只能用于日字段。指定的工作日的范围是当前搜索的月,W不能用户日期范围或日期列表。例如:15W 在日字段表示距离15日最近的工作日
  • # 用于指定月第几个周几,只能用于日字段。例如6#3 代表月第三个周五,如果日期不存在则不会触发

例子

  • 0 0 12 * * ? 每天12点触发
  • 0 0 10-20 * * ? 每天10点到20点 整点触发
  • 0 0 10,15,20 * * ? 每天10,15,20点 整点触发
  • 0 */10 * * *? 每10分钟触发
  • 0 0/10 * * * ? 每小时 0,10,20,30,40,50分时触发
  • 0 30 17 25 * ? 每月25号17点30触发
  • 0 30 17 25W * ? 每月距离25日最近的工作日 17点30分触发
  • 0 30 17 LW * ? 每月最后一个工作日17点30分触发
  • 0 30 17 ? * 6L 每月最后礼拜五17点30分触发

三、分布式锁

多机部署实例时,定时任务也会同步执行,涉及数据库操作时,会发生数据库重复写,发生不可预知的错误,这时希望一个任务只有一台机器执行。分布式锁可以解决这种问题。针对这种情况,可以分为以下两种.

3.1 Spring Scheduled

  • 如果是简单的定时任务,直接使用@Scheduled注解实现的定时任务,可以使用shedlock这个轻量级框架。官方地址:https://github.com/lukas-krecan/ShedLock

  • 使用自定义定时任务,而不是使用注解,可以考虑redis,使用setnx实现分布式锁功能

3.2 Quartz

  • TriggerListener接口的实现类可以监听触发器执行任务,可以在执行之前判断是否能拿到分布式锁,然后判断是否执行。
  • 分布式锁的实现方式有很多:常见三种:数据库行锁,redis根据setnx实现锁,和zookeeper临时顺序节点实现锁。三种方式各有优缺点,可根据项目情况选择。

参考

  • https://www.cnblogs.com/mmzs/p/10161936.html
  • https://zhuanlan.zhihu.com/p/38124817
  • https://www.cnblogs.com/liusk/p/9882217.html
  • https://docs.spring.io/spring/docs/5.2.8.RELEASE/spring-framework-reference/integration.html#scheduling

你可能感兴趣的:(springboot,java,javaweb)