前言
表达式是一个字符串,主要分成6或7个域,但至少需要6个域组成,且每个域之间以空格符隔开。
- 以7个域组成的,从右往左是【年 星期 月份 日期 小时 分钟 秒钟】
秒 分 时 日 月 星期 年
- 以6个域组成的,从右往左是【星期 月份 日期 小时 分钟 秒钟】
秒 分 时 日 月 星期
一、表达式域值说明
域 | 域值范围 | 域值占位符 | 备注 |
---|---|---|---|
秒 | 0~59 | ,-*/ | |
分 | 0~59 | ,-*/ | |
小时 | 0~23 | ,-*/ | |
日期 | 1~31或1~30 | ,- * ? / L W C | |
月份 | 1~12或JAN-DEC | , - * / | |
星期 | 1~7或SUN-SAT(SUN=1) | , - * ? / L C # | 1 表示星期天,2 表示星期一,依次类推 |
年份 | 留空或1970-2099 | , - * / | 自动生成,工具不显示该值 |
二、表达式域占位符的说明
符号 | 含义 | 示例 |
---|---|---|
* | 表示匹配域的任意值 | 在分这个域使用 *,即表示每分钟都会触发事件。 |
? | 表示匹配域的任意值,但只能用在日期和星期两个域,因为这两个域会相互影响。 | 要在每月的 20 号触发调度,不管每个月的 20 号是星期几,则只能使用如下写法:13 13 15 20 * ?。其中,因为日期域已经指定了 20 号,最后一位星期域只能用 ?,不能使用 *。如果最后一位使用 *,则表示不管星期几都会触发,与日期域的 20 号相斥,此时表达式不正确。 |
- | 表示起止范围 | 在分这个域使用 5-20,表示从 5 分到 20 分钟每分钟触发一次。 |
/ | 表示起始时间开始触发,然后每隔固定时间触发一次 | 表示起始时间开始触发,然后每隔固定时间触发一次 |
, | 表示列出枚举值 | 在分这个域使用 5,20,则意味着在 5 和 20 分每分钟触发一次。 |
L | 表示最后,只能出现在日和星期两个域 | 在星期这个域使用 5L,意味着在最后的一个星期四触发。 |
W | 表示有效工作日(周一到周五),只能出现在日这个域,系统将在离指定日期最近的有效工作日触发事件。 | 在日这个域使用 5W,如果 5 号是星期六,则将在最近的工作日星期五,即 4 号触发。如果 5 号是星期天,则在 6 号(周一)触发;如果 5 号为工作日,则就在 5 号触发。另外,W 的最近寻找不会跨过月份。 |
LW | 这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。 | |
# | 表示每个月第几个星期几,只能出现在星期这个域 | 在星期这个域使用 4#2,表示某月的第二个星期三,4 表示星期三,2 表示第二个。 |
三、示例
表达式 | 执行计划 |
---|---|
*/5 * * * * ? | 每隔 5 秒执行一次 |
0 */1 * * * ? | 每隔 1 分钟执行一次 |
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 23 * * ? | 每天 23 点执行一次 |
0 0 1 * * ? | 每天凌晨 1 点执行一次 |
0 0 1 1 * ? | 每月 1 日凌晨 1 点执行一次 |
0 0 23 L * ? | 每月最后一天 23 点执行一次 |
0 0 1 ? * L | 每周星期天凌晨 1 点执行一次 |
0 26,29,33 * * * ? | 在 26 分、29 分、33 分执行一次 |
0 0 0,13,18,21 * * ? | 每天的 0 点、13 点、18 点、21 点都执行一次 |
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 触发 |
四、简单介绍一下springboot
的几种定时任务的使用
方法一:单线程的定时器
如果有多个直接计划的情况下,可能会在运行时受到其他任务的影响
@Slf4j
@Component
@EnableScheduling
public class ScheduledConfiguration{
/**
* cron=0 0/1 * * * ?
* 1,改方法从当前日期的0分0秒开始,每间隔1分钟执行一次,每次执行的时间都是整分整秒
* 2,如果当前日期的0分0秒已过,将会从当前日期的最近一分钟的0秒开始运行
* 举例:
* 当前时间:2023-04-13 13:21:23
* 根据运算规则,已经过了整点【13:00:00】,
* 则会从最近一分钟的0秒开始,由于时间已经是21分23秒,则已经不能从21分开始,
* 那么最近的一分钟是22分,所以程序会从22分0秒开始运行,然后每间隔一分钟再运行一次
*/
@Scheduled(cron = "0 0/1 * * * ?")
private void print() {
log.info("当前时间:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
}
}
可以从下图看到确实是按照执行计划在打印时间
上述代码中主要用到了两个注解,一个是方法注解@Scheduled
,一个是类注解@EnableScheduling
。
@Scheduled
标记的方法,将会按照cron
表达式的执行计划运行方法,但要想计划生效还需要在类名上添加@EnableScheduling
注解来告诉容器,该类存在定时处理的任务,否则将不会按照计划执行方法
方法二:多线程定时任务
为了解决多任务出现冲突的情况,可以引入另一个注解@Async
,同样也需要告诉容器该类存在多线程的执行计划@EnableAsync
在看多计划任务使用多线程的方式来解决单线程执行多计划出现相互影响的问题的时候,我们先来看一下怎么在单线程的情况下混乱多个定时任务。
首先我们在类里边再加一个或多个执行计划的方法,然后在某个或多个方法中让它的运行时间加长,以此来实现打乱其他任务的执行时间,从而让执行计划在设置的时间点不能正常运行,通过此方式
我们可以看到在原定的时间点方法并没有运行,当它运行的时候,已经不是原定时间了,已经不在预定时间点,如果在这时候出现一个很长时间都无法完成的任务,你会发现原本需要处理的任务没有按时处理,这种情况下可能是致命的。废话不多说,我们来搞一下,让事件复现吧。
下面我创建两个定时任务,两个都按照间隔5
秒的计划运行代码,但我让其中一个任务执行的时间更长,让另一个定时任务在规定的时间难以运行,从而改变原有的运行时间轨迹。
@Slf4j
@Component
@EnableScheduling
public class ScheduledConfiguration {
@Scheduled(cron = "0/5 * * * * ?")
private void print1() {
log.info("1).执行计划a -> 当前时间:{},执行线程:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"), Thread.currentThread().getName());
try {
Thread.sleep(13000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Scheduled(cron = "0/5 * * * * ?")
private void print2() {
log.info("2).执行计划b -> 当前时间:{},执行线程:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"), Thread.currentThread().getName());
}
}
来看一下执行结果吧,相信不会让你失望的,你会发现第二个执行计划的执行时间已经被打乱了,并没有按照间隔5
秒运行,并且能看到执行线程都是同一个,很显然单线程的情况下是不能做到两两兼容的,而且如果某一个任务执行时间无限加长,或者卡死,那其他的任务可能都没法运行了
在讲解开始的时候我们已经了解到可以通过加两个注解的方式来解决这个问题,那究竟是不是像我们假设的这样,那让我们拭目以待吧。
首先来搞一下代码,先在两个定时任务的方法上添加注解@Async
,然后在类上添加注解@EnableAsync
@Slf4j
@Component
@EnableScheduling
@EnableAsync
public class ScheduledConfiguration {
@Async
@Scheduled(cron = "0/5 * * * * ?")
void print1() {
log.info("1).执行计划a -> 当前时间:{},执行线程:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"), Thread.currentThread().getName());
try {
Thread.sleep(13000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Async
@Scheduled(cron = "0/5 * * * * ?")
void print2() {
log.info("2).执行计划b -> 当前时间:{},执行线程:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"), Thread.currentThread().getName());
}
}
那接下来就到了我们来看结果的时候了,你会很神奇的发现居然计划b
都是按每隔5
秒运行,并且计划a
在运行的时候不管运行多久,就会按照最近时间0
秒开始,间隔5秒运行,所以秒钟永远指挥出现0
和5
,那这不就对上了吗,多线程的情况下执行计划都互不相干的按照各自的规则运行着,即使其他的计划卡死,或者时间执行再长,也不会影响其他的执行计划了
方法三:动态执行计划
顾名思义就是不用把计划写死在注解里,这种场景比较多,有时候往往要修改执行计划的时候,如果按照写死的情况,真的很不方便,为啥不方便,我就不用说了吧。
要想实现多态执行计划呢,那肯定是少不了下一丢丢功夫了,至少比之前的两种要麻烦一些,毕竟不是写在注解上,那当然需要用到spring
的接口SchedulingConfigurer
,然后实现方法configureTasks
,在方法里边写自己的动态代码也就是获取的执行计划,可以是一个配置文件,也可以是读取数据库,怎么方便怎么来吧,下面介绍修改配置文件的方式来调整执行计划。
- 配置文件添加执行计划表达式,如果不添加则使用默认计划
# 每隔5秒执行一次
spring.scheduled.cron=0/5 * * * * ?
- 配置文件搞了那就来搞一搞代码吧,废话不多说开干
@Slf4j
@Component
@EnableScheduling
public class ScheduledDynamicsConfiguration implements SchedulingConfigurer {
/**
* 这里是一个默认的执行计划,默认从0秒开始间隔5秒执行一次
*/
private static final String defaultScheduledCron = "0/5 * * * * ?";
/**
* 读取配置文件
*
* @return
*/
private Properties properties() {
Properties prop = new Properties();
try {
// 加载配置文件
prop.load(ScheduledDynamicsConfiguration.class.getResourceAsStream("/application.properties"));
} catch (Exception e) {
e.printStackTrace();
}
return prop;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(
//1.添加任务内容(Runnable)
() -> log.info("动态执行计划 -> 当前时间:{},执行线程:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"), Thread.currentThread().getName()),
//2.设置执行周期(Trigger)
triggerContext -> {
// 2.1 读取配置文件
Properties prop = properties();
//2.3 返回执行周期(Date)
return new CronTrigger(prop.getProperty("spring.scheduled.cron", defaultScheduledCron)).nextExecutionTime(triggerContext);
}
);
}
}
代码也写完了那就开始运行吧,来看看运行效果,并且我们来修改执行
- 初始
application.properties
的执行计划是间隔5
秒执行一次 spring.scheduled.cron=0/5 * * * * ?
- 运行一段时间后将执行计划修改位
3
秒执行一次 spring.scheduled.cron=0/3 * * * * ?
- 我在将执行计划修改为
13
秒执行一次 spring.scheduled.cron=0/13 * * * * ?
- 注意如果在开发阶段做测试的话你修改源码的配置文件时不会生效的,除非你做了热部署,否则你只能修改编译之后
target
下的配置文件才能生效,如果是生产环境当然就没有这个问题,毕竟生产环境的代码就是已经编译打包之后的项目。