定时任务在实际的工作中属于一项基本技能,在相对成熟的项目中,可能更多接触到的是借助xxl-job, elastic-job等来实现的分布式定时任务;在技术派项目中,当然也会介绍到定时任务这必须掌握的知识点
针对定时任务的系列教程,我们将从单机的Spring Sechedule定时任务到借助成熟的三方组件来实现的分布式定时任务逐一进行介绍说明;当然学习得由简入深,我们先来看一下基于Spring的单机定时任务是怎么来玩的
使用实例
在技术派中,直接全局搜索@Scheduled
就可以找到使用的地方
应用场景
com.github.paicoding.forum.service.sitemap.service.SitemapServiceImpl#autoRefreshCache
/**
* 采用定时器方案,每天5:15分刷新站点地图,确保数据的一致性
*/
@Scheduled(cron = "0 15 5 * * ?")
public void autoRefreshCache() {
log.info("开始刷新sitemap.xml的url地址,避免出现数据不一致问题!");
refreshSitemap();
log.info("刷新完成!");
}
上面这个定时任务干的时间也很简单,每天5:15分刷新站点地图,用于seo的sitemap.xml文件生成
若我们的实际项目中需要使用定时任务,还需要主动开启,一般常见于在启动类上,添加注解@EnableScheduling
基于上面的配置完成之后,一个定时任务就算是完成了; 整体的使用感官上来说,非常简单
基本知识点
从技术派的使用角度来看,会发现很简单,没什么可说的;但是,这里面涉及到的知识点其实非常多,我们来逐一进行说明
cron表达式
Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式:
Seconds Minutes Hours DayofMonth Month DayofWeek Year
Seconds Minutes Hours DayofMonth Month DayofWeek
上面每个坑位,可以取得值不一样,先分别说明几个可能见到的符号
*
: 表示匹配该域的任意值,如分钟的坑位为*
, 表示每分钟都会触发?
: 只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和 DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法:13 13 15 20 ?
-
: 表示范围,例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次。/
: 表示起始时间开始触发,然后每隔固定时间触发一次- 如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次
,
: 表示列出枚举值值。- 如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
L
: 表示最后,只能出现在DayofWeek和DayofMonth域,- 如在DayofWeek域使用5L,意味着在最后的一个星期四触发。
W
: 表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件- 如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一 到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份。
LW
: 这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。#
: 用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。
根据上面的说明,前面的crond表达式含义就比较清楚了
0/1 * * * * ?
每s种执行一次
几个简单的实例进行说明
"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触发
表达式增强
在Spring5.3(Spring Boot 2.4)及之后的版本,对crond表达式进行了增强
1. 宏指令
最主要的特点就是增加了一些宏指令
宏 | 表达式 | 说明 |
---|---|---|
@yearly | 0 0 0 1 1 * | 每年的1月1号0点执行一次 |
@monthly | 0 0 0 1 * * | 每月一次 |
@weekly | 0 0 0 * * 0 | 每周一次 |
@daily 或 @midnight | 0 0 0 * * * | 每天一次 |
@hourly | 0 0 * * * * | 每小时一次 |
2. 最后几天
每周的第几天
v
* * * * * *
^
每月的第几天
如上其中的 每月的第几天、每周的第几天 支持 最后几天 (L) 的语义 例如:
"0 0 0 L * *" 每月的最后一天零点
"0 0 0 L-3 * *" 每月的最后三天零点
"0 0 0 * * 5L" 每月最后的周五零点
"0 0 0 * * THUL" 每月最后的周三零点
3. 增强原有表达式 工作日
* * * * * *
^
每月的第几天
如上其中的 每月的第几天 支持 工作日 (W)的语义 例如:
"0 0 0 1W * *" 每月的第一个工作日零时
"0 0 0 LW * *" 每月的最后一个工作日零时
4. 增强原有表达式 几周的星期几
每周的第几天
v
* * * * * *
如上其中的 每周的第几天 支持 每月第几周的第几天语义 例如
"0 0 0 ? * 5#2" 每月第二周的星期五零时
"0 0 0 ? * MON#1" 每月周一的星期一零时
多定时任务执行顺序
如何确认一个项目中的多个定时任务是串行执行还是并发执行呢?要想验证这个功能,最好的法子就是写个testcase,比如定义两个定时任务,在其中一个任务中写个死循环,看另外一个任务是否会正常执行
@Scheduled(cron = "0/1 * * * * ?")
public void sc1() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " | sc1 " + System.currentTimeMillis());
while (true) {
Thread.sleep(5000);
}
}
@Scheduled(cron = "0/1 * * * * ?")
public void sc2() {
System.out.println(Thread.currentThread().getName() + " | sc2 " + System.currentTimeMillis());
}
首先我们分析的是 sc1和sc2这两个任务的执行是串行还是并行的,暂时先不考虑 sc1 调用时阻塞,下一秒是否是开新的线程再调用sc1
- 若串行:则sc1打印一次,sc2可能打印0或者1次
- 若并行:sc1打印一次,sc2打印n多次
实际运行,GIF图演示如下
上图的结果,印证了默认的情况下,多个定时任务时串行执行的;如果一个任务出现阻塞,其他的任务都会受到影响
定时任务执行的优先级
既然是顺序执行的,那么优先级怎么定?每次都是固定的,还是随机的呢?
要验证上面的方法,也容易,同样两个任务,看他们的输出是否会乱掉,如果每次都是任务1打印完再打印任务2,那就是固定优先级的;否则每次调度时,顺序不好说
测试代码如下
@Scheduled(cron = "0/1 * * * * ?")
public void sc1() {
System.out.println(Thread.currentThread().getName() + " | sc1 " + System.currentTimeMillis());
}
@Scheduled(cron = "0/1 * * * * ?")
public void sc2() {
System.out.println(Thread.currentThread().getName() + " | sc2 " + System.currentTimeMillis());
}
实测结果如下
从输出得出结论:顺序是串掉的,并没有表现出明显的优先级关系
并行调度
接下来的问题就是我希望这些任务可以并发执行,可以实现么?
当然是可以,用起来也比较简单,首先是在Application上添加注解@EnableAsync
,开启异步调用,然后再计划任务上加上@Async
注解即可,一个简单的demo如下
@EnableAsync
@EnableScheduling
@SpringBootApplication
public class QuickMediaApplication {
public static void main(String[] args) {
SpringApplication.run(QuickMediaApplication.class, args);
}
@Scheduled(cron = "0/1 * * * * ?")
@Async
public void sc1() {
System.out.println(Thread.currentThread().getName() + " | sc1 " + System.currentTimeMillis());
}
}
上面执行之后,查看输出(异步调度时,理论上线程名应该不一样)
从上面的输出,可以简单的推理,每次调度上面的任务都是新开了一个线程来做的,所以如果在定时任务中写了死循环,是否会导致无限线程,最后整个进程崩掉?
额外提一句,linux系统下单进程的线程数是有上限的,查看命令为:
ulimit -u
在测试之前,先看下上面的正常任务执行,如下面的动图,线程数并没有夸张的长法
接下来换成死循环的调度方式,实际测试如下,线程数蹭蹭的上涨
所以使用默认的异步调用方式,并不是一个好注意,说不准就被玩死了自己都不知道,那么可以用自己的线程池来管理这些异步任务么?
自定义线程池
用自定义的线程池来取代默认线程管理方式,无疑是一个更加安全和灵活的方式,使用起来也并不麻烦,和平常创建线程池的套路没什么区别,要在Spring生态中使用,就把它搞成bean即可
直接借助Spring的线程池ThreadPoolTaskExecutor
@Bean
public AsyncTaskExecutor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("yhh-schedule-");
executor.setMaxPoolSize(10);
executor.setCorePoolSize(3);
executor.setQueueCapacity(0);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
return executor;
}
@Scheduled(cron = "0/1 * * * * ?")
@Async
public void sc1() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " | sc1 " + System.currentTimeMillis());
while (true) {
Thread.sleep(1000 * 5);
}
}
实际演示的结果如下,最多10个线程,再提交的任务直接丢弃
简单说一下,用自定义线程池的好处:
- 合理的分配线程池参数
- 拒绝策略的选择也比较有意思(可以按照自己的想法来处理"负载"的任务)
- 线程池命名,对于以后问题排查,会有很大的帮助
小结
关于Spring定时任务的相关知识点内容,核心信息如下:
使用姿势
首先通过添加注解@Sscheduling
开启定时任务;其次在需要的公共方法上,添加Scheduled
即可实现一个定时任务
定时任务知识点
- crond 表达式
- 默认所有的定时任务都是串行调度的,一个线程,且即便crond完全相同的两个任务先后顺序也没法保证(具体原因需要源码分析,看下这块是怎么支持)
- 使用
@Async
注解可以使定时任务异步调度;但是需要开启配置,在启动类上添加@EnableAsync
注解 - 开启并发执行时,推荐用自定义的线程池来替代默认的,理由如上
最后再抛出一个问题:
- 当项目中有多个定时任务都是晚上11点执行,如果其中一个任务执行抛出异常,但是业务代码中没有补货异常,那么其他的定时任务会受到影响么?能正常执行么?
- 欢迎在评论区给出你的答案
知识星球
目前技术派已经整理出 89 篇文章(已完成 83 篇
,✅表示已经完成),为了方便大家学习,文章标题后面追加了 2 个标签,分别为“🌟新人必看”和“👍强烈推荐”,方便大家查阅,妥妥细节控~~
技术派教程是星球推出的主打服务,推出的「技术派」开源项目,已收获 1000+ Star,除此之外,还包括其它多项福利,详见 技术派知识星球 。
原价 129 元,送大家一张 30 元优惠券,券后仅 99 元。
说明:楼仔的「技术派」星球,和沉默王二的「Java程序员进阶之路」星球合并了,之前是发的“技术派”的星球优惠券,大家可以直接进入二哥的星球,除了以上所说的内容,还能享受更多福利。
如果觉得不满意,支持 3 天无理由退款哈~~
回复