本文我们聊一下Spring定时任务调度。这个主题,相信大家都比较熟悉,平时用的应该也比较多。不过说起实现原理,了解的小伙伴应该就不太多了,网上对应的文章虽然很多,不过基本都是使用教程。即使有少部分提及原理,说的也是含糊不清,更有甚者是胡说八道。
其实Spring的定时任务调度,并不神秘,也不是Spring特有的,本质还是借助JDK的能力实现的,只是在使用方式上更Spring一点,也就是更简洁、更便捷一点。
关于JDK的定时任务,主要是借助
ScheduledThreadPoolExecutor
实现的,感兴趣的小伙伴可以可以自行了解其实现原理。这里我们主要聊Spring的相关细节,关于JDK部分我们不会详细介绍。当然,如果你们有需要,恰巧我也有时间的话,也可以拿出来说一说,毕竟说啥不是说呢,是吧。
1. Spring定时任务的类型
关于Spring定时任务的使用,小伙伴们应该都比较熟悉,就是直接在方法上加上@Scheduled
注解即可。Spring会根据你指定的频率,定时调度该方法的执行,这一点完全不用你关心。
使用自然是很简单的,这是Spring一贯的风格。关于Spring是怎么做的,小伙伴应该也可以猜到,至少得先把这些标注了@Scheduled
注解的定时方法找出来,然后在想办法让它定时执行。当然前面我们也说了,这部分是借助JDK的能力来实现的。
但是在使用Spring定时调度的过程,也有一些细节需要先和小伙伴们介绍清楚。首先就是Spring支持的三种类型任务的执行逻辑,这里恐怕能说清楚的小伙伴们不多,尤其是在碰到单线程模型,我们先梳理一下:
Spring支持CRON表达式类型、fixedDelay间隔执行、fixedRate间隔执行三种任务类型。关于这三种类型在任务执行上的差别,我们一一介绍一下。
1.1 CRON表达式类型任务
关于CRON表达式含义,这里我们不再介绍,相信小伙伴们都比较熟悉,实在不熟悉自行查阅资料吧。
我们要说的是,在单线程执行的情况下,如果CRON任务执行时间过长,以至于下次执行的时间都到了,但是上次任务还没有执行结束,下次任务要怎么办。
这里先给结论:放弃
,也就是下一次任务执行就被放弃了,也就是少执行了一次
。这里拿任务设置为每五秒执行一次的表达式,说明一下:
- 假设10:00:00s时,任务第一次执行,但是任务执行时间很长,执行了7s。
- 根据任务的执行计划,10:00:05s时,应该要执行第二次任务,但是此时发现有任务在执行(上一次任务需要执行到10:00:07s),那么,
此次执行计划直接放弃,也就是本次任务不执行了
。 - 根据任务的执行计划,10:00:10s时,应该要执行第三次任务,此时发现没有任务执行,本次任务正常执行。
这里大家一定要注意,单线程模型下,由于第一次任务执行时间较长,导致第二次任务不执行,也就是少执行了一次。这里可能会影响预期、从而产生业务影响。
1.2 fixedDelay间隔类型任务
fixedDelay是最简单的一种方式模型,间隔执行:也就是延迟指定的间隔后,再次执行下次任务。计算公式为:下次执行时间 = 上次任务执行结束时间 + 间隔时间
。相同的问题:如果任务执行时间较长,下次执行时间也会晚于预期。这里以任务间隔为五秒,说明一下:
- 假设10:00:00s时,任务第一次执行,任务执行时间较长,执行了7s。
- 第一次任务执行结束后(10:00:07),等待5s后,再次执行下一次任务(10:00:12),第二次任务执行了3s。
- 第二次任务执行结束后(10:00:15),等待5s后,再次执行下一次任务,依此类推。
这里需要注意,单线程模型下,如果存在任务执行时间较长,整体的执行计划都会往后顺延
。
1.3 fixedRate间隔类型任务
fixedRate也是间隔执行的方式,只是这个间隔不是按照任务结束时间计算的,而是按照开始时间。计算公式为:下次执行时间 = 上次任务执行开始时间 + 间隔时间
。当然,如果任务执行时间较长,超过间隔时间,下次执行时间也要顺延,毕竟不能强暴的直接打断吧。
不过fixedRate会将间隔会自动缩小,尽量追赶计划执行时间,一旦赶上或者追平,继续按照指定间隔执行。这里还是以间隔为五秒的情况,说明一下:
- 假设10:00:00s时,任务第一次执行,任务执行时间较长,执行了7s。
- 根据任务的执行计划,10:00:05s时,应该要执行第二次任务,但是此时第一次任务还在执行中,所以第二次执行时间只能等待顺延。
- 第一次任务执行结束后(10:00:07),发现已经晚于第二次执行的计划时间了。会追赶进度,所以第二次任务立即执行。
- 这里假设第二次任务只需要执行2s,在10:00:09就执行结束了。计划第三次执行时间为:10:00:10,也就是第二次任务已经追平了,无需继续追赶,此时会遵循计划,在10:00:10时,正常执行第三次任务。
这里需要注意,fixedRate会自动调整间隔,使任务尽快追平计划时间,追平后遵循计划执行
。当然这里讨论的也是单线程模型下。
好了,关于定时任务的三种类型的讨论就这么多。大家注意在单线程模型下,上面的讨论才有意义。大家清楚不同任务类型,发生任务执行时间过长,对下次执行时间的影响即可。再次强调,是单线程模型下,如果是多线程执行,影响情况需要结合线程池配置分析了,这里我们不具备讨论条件。
这里为什么执着的讨论单线程模型,因为Spring默认的就是单线程模型,而往往我们又不指定调度线程池。所以其实单线程模型才是最最常用的。
2. @Scheduled注解解析
通过上一章节对Spring三种定时任务类型的介绍,相信小伙伴们已经很清楚他们之间的区别了。在Spring中,定时任务都是由@Scheduled标识的,三种任务类型分别对应@Scheduled的三种属性,分别是cron
、fixedDelay
、fixedRate
,设置对应的值,即为开启对应类型的任务。
我们在上面也介绍过了,Spring要执行这些定时任务,第一步就是需要先解析出来这些定时任务,然后才能交由JDK处理。那么本章节我们就来看一下解析过程。
2.1 @EnableScheduling开启任务调度功能
在探索解析流程之前,我们先介绍一下@EnableScheduling
。大家知道,在使用Spring的定时任务调度功能前,是需要在类上先添加@EnableScheduling开启的,这究竟有什么用呢。
关于Spring的@EnableXXX
,通常到时开启某种能力,比如EnableScheduling开启定时任务调度、 @EnableAsync开启异步调用等。其实原理也很简单,都是借助@Import
能力导入某些BeanPostProcessor
(也有可能是其他类型的),这些BeanPostProcessor,会在bean的生命周期的各个流程发挥重要作用,从而使Spring具有强大的能力。
关键这个过程完全是可插拔的,加入某个BeanPostProcessor,就具备响应能力了,拓展能力极强。这也正是@EnableXXX
的原理,可以简单理解为:开启某项功能。
BeanPostProcessor
是Spring留给我们的一种拓展方式,能力非常强悍,甚至很多Spring的核心能力,比如@AutoWired
和@Resource
属性注入,都是借助它实现