SpringBoot学习小结之定时任务

25 篇文章 3 订阅
18 篇文章 0 订阅

一、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表达式是由六个或七个子表达式(字段)组成的字符串,用于描述计划的各个详细信息

秒 分 时 日 月 周 年

字段名字是否必填允许值允许特殊字符
Y0-59, - * /
Y0-59, - * /
Y0-23, - * /
Y1-31, - * / ? L W C
Y0-11 或 JAN-DEC, - * /
Y1-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临时顺序节点实现锁。三种方式各有优缺点,可根据项目情况选择。

参考

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式: Seconds Minutes Hours DayofMonth Month DayofWeek Year或 Seconds Minutes Hours DayofMonth Month DayofWeek 每一个域可出现的字符如下: Seconds:可出现", - * /"四个字符,有效范围为0-59的整数 Minutes:可出现", - * /"四个字符,有效范围为0-59的整数 Hours:可出现", - * /"四个字符,有效范围为0-23的整数 DayofMonth:可出现", - * / ? L W C"八个字符,有效范围为0-31的整数 Month:可出现", - * /"四个字符,有效范围为1-12的整数或JAN-DEc DayofWeek:可出现", - * / ? L C #"八个字符,有效范围为1-7的整数或SUN-SAT两个范围。1表示星期天,2表示星期一, 依次类推 Year:可出现", - * /"四个字符,有效范围为1970-2099年 每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是: (1)*:表示匹配该域的任意值,假如在Minutes域使用*, 即表示每分钟都会触发事件。 (2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。 (3)-:表示范围,例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次 (4)/:表示起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次. (5),:表示列出枚举值值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。 (6)L:表示最后,只能出现在DayofWeek和DayofMonth域,如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。 (7)W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 (8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。 (9)#:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。 举几个例子: 0 0 2 1 * ? * 表示在每月的1日的凌晨2点调度任务 0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业 0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作 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触发
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aabond

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值