spring @Scheduled原理解析

·  阅读 5111

本文主要分析@Scheduled fixedDelay fixedRate cron 的运行结果以及源码探究

                      目录

  1. 为什么写本文
  2. spring @Scheduled执行结果
  3. spring @Scheduled源码探究
  4. 总结
  5. 参考文章


环境

spring版本4.3.7.RELEASE
jdk版本1.8.0_201
测试平台win10

1.为什么写本文

我们线上某个任务每个小时定时拉取ftp文件,之前一直都是正常的。忽然有一天下午,有人反馈说文件没拉取到。通过日志可以看到某个时间点执行拉取任务时忽然就没日志了。

后续的拉取任务就没有在继续执行。

看到这里就很奇怪了,我们不是用了线程池吗?为什么一个任务卡住了,后面每个小时的任务也不执行了。而且从日志中看到,卡死的时候使用的线程池中的scheduler-2线程,后面日志中scheduler-2也没有继续执行其他任务了。带着这些疑问,我去研究了一下spring @Scheduled原理。

2.spring @Scheduled执行结果

首先我们来给出3总模式下面运行的结果吧,然后从结果出发,结合源码来分析。

一般情况下@Scheduled有如下3种使用方式,fixedDelay fixedRate cron ,代码如下图。

@Component
public class ScheduleTest {
    private Logger logger = LoggerFactory.getLogger(getClass());

    private List<Integer> index = Arrays.asList(8 * 1000, 3 * 1000, 6 * 1000, 2 * 1000, 2 * 1000);

    private AtomicInteger count = new AtomicInteger(0);

    @Scheduled(fixedDelay = 3 * 1000)
//    @Scheduled(fixedRate = 5 * 1000)
//    @Scheduled(cron = "0/5 * * * *  ?")
    public void testSchedule() throws InterruptedException {
        int i = count.get();
        if (i < 5) {
            Integer sleepTime = index.get(i);
            logger.info("第{}个任务开始执行,执行时间为{}ms", i, sleepTime);
            Thread.sleep(sleepTime);
            count.getAndIncrement();
        }
    }
}复制代码

 这三种方式对应的运行结果如下

fixedDelay运行结果

2019-07-16 11:11:36,033 INFO [org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler] - <Initializing ExecutorService  'scheduler'>
2019-07-16 11:11:36,064 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第0个任务开始执行,执行时间为8000ms>
2019-07-16 11:11:47,064 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第1个任务开始执行,执行时间为3000ms>
2019-07-16 11:11:53,065 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第2个任务开始执行,执行时间为6000ms>
2019-07-16 11:12:02,065 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第3个任务开始执行,执行时间为2000ms>
2019-07-16 11:12:07,080 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第4个任务开始执行,执行时间为2000ms>复制代码

该方式最简单,在上一个任务执行完成之后,间隔3秒(因为@Scheduled(fixedDelay = 3 * 1000))后,执行下一个任务.这种是最容易理解的,所以放在第一个来讲.用一个图来表示的话,更容易理解.如下:


fixedRate运行结果

2019-07-16 11:19:38,670 INFO [org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler] - <Initializing ExecutorService  'scheduler'>
2019-07-16 11:19:38,701 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第0个任务开始执行,执行时间为8000ms>
2019-07-16 11:19:46,702 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第1个任务开始执行,执行时间为3000ms>
2019-07-16 11:19:49,702 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第2个任务开始执行,执行时间为6000ms>
2019-07-16 11:19:55,702 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第3个任务开始执行,执行时间为2000ms>
2019-07-16 11:19:58,718 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第4个任务开始执行,执行时间为2000ms>复制代码

用一句话来说就是,如果前一个任务执行时间(这个时间是累计的)超过执行周期,则后一个任务在前一个任务完成后立即执行,否则等待到指定周期时刻执行。

用一个图来表示的话,更容易理解.如下:


cron 运行结果

2019-07-16 11:29:48,623 INFO [org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler] - <Initializing ExecutorService  'scheduler'>
2019-07-16 11:29:50,014 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第0个任务开始执行,执行时间为8000ms>
2019-07-16 11:30:00,014 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第1个任务开始执行,执行时间为3000ms>
2019-07-16 11:30:05,014 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第2个任务开始执行,执行时间为6000ms>
2019-07-16 11:30:15,015 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第3个任务开始执行,执行时间为2000ms>
2019-07-16 11:30:20,015 INFO [cn.webank.cnc.bdrs.data.service.ScheduleTest] - <第4个任务开始执行,执行时间为2000ms>复制代码

用一句话来说就是,每隔5S就会来询问是否可以执行下一个任务,如果前一个任务没有执行完成,则后面的任务需要继续等待5S后再来问,看看是否可以执行。

用一个图来表示的话,更容易理解.如下:


小结

从上面3中运行结果可以看出,spring @Scheduled执行的定时任务,都会依赖前一个任务的执行情况。我们在项目中是用的cron这种方式,当14:30:20的任务卡死后,后面的任务都无法按照预设的时间执行了。为什么会这样呢?我们来看看它的实现原理。

3.spring @Scheduled源码探究

定时任务调度的基础是ScheduledAnnotationBeanPostProcessor类,查看继承体系发现该类实现了BeanPostProcessor接口,所以进入该类的postProcessAfterInitialization方法。


该方法先查找被Scheduled注解标注的类 ,如果被Scheduled注解标注,就执行processScheduled

方法。

进入processScheduled方法。我是用cron表达式的方式进行debug的。


上图代码块,判断cron属性不为空,初始化任务,放入tasks( LinkedHashSet)中。

继续往下debug


上图代码块,按照bean分类,将每个bean的定时任务存进scheduledTasks。

至此postProcessAfterInitialization方法执行完成。

在该方法中,spring主要就是解析注解,并将根据注解生成相应的延时任务。

那么现在解析好了,也存储好了,执行的地方在哪里呢?

在一次查看该类的继承体系,发现该类还实现了ApplicationListener接口,所以

进入onApplicationEvent方法。


没啥好讲的,直接进入finishRegistration方法。

finishRegistration一大段都是判断TaskScheduler是否存在。直接看到最后一行

this.registrar.afterPropertiesSet();复制代码

 进入afterPropertiesSet

/**
 * Calls {@link #scheduleTasks()} at bean construction time.
 */
@Override
public void afterPropertiesSet() {
	scheduleTasks();
}复制代码

 没啥好讲,进入scheduleTasks


上面代码,第一个if 判断是否有线程池(这里我配置了线程池,配置见下图,所以不为空)

<task:scheduler id="scheduler" pool-size="10"/>复制代码

 如果没配置默认为Executors.newSingleThreadScheduledExecutor();

其他if是判断是哪种任务,triggerTasks任务貌似不是通过注解进入的先忽略。

关于线程池这里多说两句,从最开始测试结果的日志中可以看到spring初始化线程池的地方

2019-07-16 11:29:48,623 INFO [org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler] - <Initializing ExecutorService  'scheduler'>复制代码

先中断这次调试,在org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler

initializeExecutor方法加上断点,再次启动服务进行debug。(可以先在scheduleTasks 方法加上断点,方便等下重新回到这里)


进入createExecutor 方法


从上图代码块可知,spring默认的线程池使用的是 ScheduledThreadPoolExecutor。熟悉ScheduledThreadPoolExecutor的朋友们应该知道,ScheduledThreadPoolExecutor中有schedule,scheduleAtFixedRate,scheduleWithFixedDelay这3个方法。这和@Scheduled中的cron,fixedRate,fixedDelay。看上去是不是很像。我们继续进行调试。

我们继续按照之前的debug流程进入到 scheduleTasks 方法

因为我的是cron任务,所以进入了

if (this.cronTasks != null) {
	for (CronTask task : this.cronTasks) {
		addScheduledTask(scheduleCronTask(task));
	}
}复制代码

 进入scheduleCronTask方法


进入this.taskScheduler.schedule方法,选择ThreadPoolTaskScheduler的实现。

ReschedulingRunnable是一个实现了Runnable接口的对象

我们直接看ReschedulingRunnable调用的schedule方法。

public ScheduledFuture<?> schedule() {
	synchronized (this.triggerContextMonitor) {
		this.scheduledExecutionTime = this.trigger.nextExecutionTime(this.triggerContext);
		if (this.scheduledExecutionTime == null) {
			return null;
		}
		//计算下次执行时间
		long initialDelay = this.scheduledExecutionTime.getTime() - System.currentTimeMillis();
		//将自己传入执行器,也就是调用自己的run方法
		this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS);
		return this;
	}
}复制代码

 进入ReschedulingRunnable 的 run方法

@Override
public void run() {
	Date actualExecutionTime = new Date();
	//执行我们定义的定时任务
	super.run();
	Date completionTime = new Date();
	synchronized (this.triggerContextMonitor) {
		//更新时间
		this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime);
		if (!this.currentFuture.isCancelled()) {
			//再次调用schedule方法
			schedule();
		}
	}
}复制代码

经过上面的分析,我们可以得出执行器的schedule方法只会执行一次,所以spring在这个地方使用互相调用的方法,来达到定时循环的目的。所以这个方法中,关键的就是时间的更新。

回看this.trigger.nextExecutionTime(this.triggerContext)方法,选择CronTrigger的实现。

@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
	//获取上一次任务完成时间
	Date date = triggerContext.lastCompletionTime();
	if (date != null) {
		//获取上一次任务执行时间
		Date scheduled = triggerContext.lastScheduledExecutionTime();
		if (scheduled != null && date.before(scheduled)) {
			//比较两次时间,大的生成新的执行时间
			date = scheduled;
		}
	}
	else {
		//初始化的时候直接使用当前时间
		date = new Date();
	}
	return this.sequenceGenerator.next(date);
}复制代码

ron模式每次根据上次执行时间和上次完成时间更新后面的生成新的时间。

所以当某个任务卡死的时候,就无法更新时间,后面的任务就没法定时执行了。另外两种方法就

另外两种模式是执行的是ScheduledThreadPoolExecutor的对应方法(scheduleAtFixedRate,scheduleWithFixedDelay)。具体原理有兴趣的同学可以去阅读下对应源码。它们也是需要根据上次执行时间和上次完成时间更新后面的生成新的时间。重点看下ScheduledThreadPoolExecutor#setNextRunTime方法。

/**
 * Sets the next time to run for a periodic task.
 */
private void setNextRunTime() {
	long p = period;
	if (p > 0)
		time += p;
	else
		time = triggerTime(-p);
}复制代码

4.总结

spring @Scheduled默认实现的定时任务是有前后依赖关系的。我们在使用的时候需要考虑到这一点。

5.参考文章

www.jianshu.com/p/925dba9f5…

mp.weixin.qq.com/s/1IyXrkhCv…

juejin.cn/post/684490…


分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改