@Scheduled是springboot自带的定时任务注解,可以快速实现任务的定时执行,使用方法是直接在要实现定时任务的方法上加上@Scheduled注解。但是需要特别注意的是,在使用这个注解时应现在启动类上加上@EnableScheduling注解,表示可以开启定时任务的注解。
一. 基本用法
-
@Scheduled(fixedDelay = 1000)
上一个任务结束到下一个任务开始的时间间隔为固定的1秒,任务的执行总是要先等到上一个任务的执行结束 -
@Scheduled(fixedRate = 1000)
每间隔1秒钟就会执行任务(如果任务执行的时间超过1秒,则下一个任务在上一个任务结束之后立即执行) -
@Scheduled(fixedDelay = 1000, initialDelay = 2000)
第一次执行的任务将会延迟2秒钟后才会启动 -
@Scheduled(cron = “0 15 10 15 * ?”)
Cron表达式,每个月的15号上午10点15分开始执行任务
二. Cron表达式
Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式:
(1) Seconds Minutes Hours DayofMonth Month DayofWeek Year
(2)Seconds Minutes Hours DayofMonth Month DayofWeek
常用通配符:
*:表示所有值 比如用在日 表示每一天。
?:表示不指定值 比如周配置 表示不指定星期几执行。
/:表示递增触发 比如 用在分 5/20 从第五分钟开始 每增加20分钟执行一次。
-:表示区间 比如用在 1-6 表示一月到六月执行。
示例:
每隔5秒执行一次:*/5 * * * * ?
每隔1分钟执行一次:0 */1 * * * ?
每天23点执行一次:0 0 23 * * ?
每天凌晨1点执行一次:0 0 1 * * ?
每月1号凌晨1点执行一次:0 0 1 1 * ?
每月最后一天23点执行一次:0 0 23 L * ?
每周星期天凌晨1点实行一次:0 0 1 ? * L
三.@Scheduled问题
1.@Scheduled 单线程堵塞问题,因为@schedule注解默认是单线程的,如果定时任务比较多或者有的定时任务比较耗时,会影响到其他定时任务的执行。解决方案是将@Schedule改为多线程执行。有如下两种配置方式。
第一种:增加配置类
@Configuration
public class ScheduleConfig {
/**
* 修复同一时间无法执行多个定时任务问题。@Scheduled默认是单线程的
*/
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
//核心线程池数量,方法: 返回可用处理器的Java虚拟机的数量。
taskScheduler.setPoolSize(Runtime.getRuntime().availableProcessors() * 2);
return taskScheduler;
}
}
第二种:与第一种类似
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.setScheduler(
new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors() * 2)
);
}
}
2.@Scheduled使用时是在本机进行任务调度,但是目前几乎所有的应用为了增加发负载量,都是使用多机部署。这就导致了一个非常严重的分布式问题:在每一台机器上都会在同时执行定时调度任务,可能产生很多重复数据或者导致系统出现其他的业务逻辑BUG,所以在使用@Scheduled进行任务调度时,一定要配合redis的分布式锁来使用,让定时调度任务只在一台机器上执行,避免BUG出现。redis分布式所解决方案如下:
@Scheduled(fixedRate = 60000)
public void executeImportFile() {
String redisKey = "REDIS:KEY";
String lock = "";
try {
// 获取分布式锁
lock = redisUtils.tryLock(redisKey, 30, TimeUnit.MINUTES);
if (!StringUtils.isEmpty(lock)) { // 加锁成功
// TODO 执行定时任务
} else { // 加锁失败
// TODO 不做处理
}
} catch (Exception e) {
log.error("调度线程执行异常,{}", e);
} finally {
if (!StringUtils.isEmpty(lock)) { // lock不为空 释放分布式锁
Boolean unLock = redisUtils.unLock(redisKey, lock);
log.info("调度任务正在释放锁,解锁状态:" + unLock + ",解锁" + (unLock ? "成功" : "失败"));
}
}
}