一、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 主要接口

  • SchedulerQuartz实现任务调用最主要的接口,它包含JobDetailTrigger的注册和调度器的执行
  • SchedulerFactory提供获取Scheduler的工厂接口,一般使用quartz提供实现类StdSchedulerFactory
  • Trigger触发器接口,和Job执行相关,包含触发开始时间,结束时间, 下次开始时间等信息
  • SimpleTrigger给定时间间隔触发,还可以指定次数
  • CronTriggercron表达式触发
  • CalendarIntervalTriggerSimpleTrigger类似,间隔时间触发,不同的是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临时顺序节点实现锁。三种方式各有优缺点,可根据项目情况选择。

参考