一、Spring自带定时任务
Spring自带定时任务相关类位于spring-context包
1.1 注解
-
@Scheduled
标记方法定时执行。所标记的方法必须没有参数,返回值会被忽视。以下属性必须满足一个:
cron
支持cron表达式,不支持year字段fixedDelay
上次调用结束和下次调用开始间隔时间,单位毫秒fixedDelayString
支持毫秒字符串、占位符、符合java.time.Duration
解析的字符串fixedRate
上次调用开始和下次调用开始间隔时间,单位毫秒fixedRateString
和fixedDelayString
格式类似
还有两个延迟属性
initialDelay
初始延迟毫秒数,默认-1initialDelayString
和fixedDelayString
格式类似
-
@EnableScheduling
开启任务调度,可以和
@SpringBootApplication
一起使用,或者和@Configuration
一起,会确保配置类中或者使用@ComponentScan
所扫描到的带有@Scheduled
注解的Bean任务执行。默认情况下,会在配置类中优先搜索
org.springframework.scheduling.TaskScheduler
类型或名字为taskScheduler
的Bean,如果没找到会搜索java.util.concurrent.ScheduledExecutorService
类型的Bean,如果二者都没找到,会默认通过ConcurrentTaskScheduler
创建一个单线程的ScheduledExecutorService
( SpringBoot 2.1.0 后会默认创建ThreadPoolTaskExecutor
不再是单线程)Executors.newSingleThreadScheduledExecutor()
-
@Async
标记目标方法异步执行,标记类代表所有方法异步执行,不能标记带有
@Configuration
的类。目标方法返回值被限定为
void
、java.util.concurrent.Future
- value 值是类型为
java.util.concurrent.Executor
或org.springframework.core.task.TaskExecutor
的Bean名字,存在多个实例,可以指定Bean名字执行方法
- value 值是类型为
-
@EnableAsync
开启异步方法执行,和
@EnableScheduling
类似,会搜索org.springframework.core.task.TaskExecutor
,或名字叫taskExecutor
的java.util.concurrent.Executor
类型的Bean,两个都没找到会默认创建org.springframework.core.task.SimpleAsyncTaskExecutor
。返回值为void的方法,异常不能被捕获,为了解决这个问题,可以通过实现
AsyncConfigurer
接口自定义配置。
1.2 配置
springboot 2.1.0之后自带任务调度器,可以通过以下参数进行配置,也可以自己使用@Configuration
自定义不同的任务调度器
spring:
task:
execution:
pool:
allow-core-thread-timeout: true # 是否允许核心线程超时
core-size: 8 # 核心线程数量
keep-alive: 60s #存活时间
max-size: Integer.MAX_VALUE # 最大线程数量
queue-capacity: Integer.MAX_VALUE #队列容量
1.3 主要类
-
ScheduledAnnotationBeanPostProcessor
主要是通过这个类来处理定时任务,这个类是Bean的生命周期接口
BeanPostProcessor
的实现类。-
postProcessAfterInitialization
通过这个方法查找Bean中
@Scheduled
或@Schedules
标记的类和方法public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler || bean instanceof ScheduledExecutorService) { // Ignore AOP infrastructure such as scoped proxies. return bean; } Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean); if (!this.nonAnnotatedClasses.contains(targetClass) && AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) { Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass, (MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> { Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations( method, Scheduled.class, Schedules.class); return (!scheduledMethods.isEmpty() ? scheduledMethods : null); }); if (annotatedMethods.isEmpty()) { this.nonAnnotatedClasses.add(targetClass); } else { annotatedMethods.forEach((method, scheduledMethods) -> scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean))); } } return bean; }
-
processScheduled
通过此方法来解析定时任务方法,三种注解所带来不同的定时任务
-
-
ScheduledTaskRegistrar
定时任务注册器,可通过此类在配置类中添加定时任务。
1.4 动态定时任务
定时任务执行的cron表达式存放在数据库中,可动态修改动态执行
DROP TABLE IF EXISTS `cron`;
CREATE TABLE `cron` (
`id` int(11) NOT NULL,
`cron` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
spring:
datasource:
username:
password:
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useSSL=false&useUnicode=true
driver-class-name: com.mysql.jdbc.Driver
@Mapper
@Repository
public interface CronMapper {
@Select("select cron from cron limit 1")
String getCron();
}
public class OneTask implements Runnable{
private static final Logger logger = LoggerFactory.getLogger(OneTask.class);
@Override
public void run() {
logger.info("->执行");
}
}
@Configuration
@EnableScheduling
public class MyScheduleConfig implements SchedulingConfigurer {
private static final Logger logger = LoggerFactory.getLogger(MyScheduleConfig.class);
@Autowired
private CronMapper mapper;
private String preCron;
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.addTriggerTask(new OneTask(), triggerContext -> {
String cron = mapper.getCron();
// 校验cron表达式是否合法
if (!CronSequenceGenerator.isValidExpression(cron)) {
logger.info("数据库cron表达式非法:{}", cron);
return new CronTrigger( preCron).nextExecutionTime(triggerContext);
}
preCron = cron;
return new CronTrigger(cron).nextExecutionTime(triggerContext);
});
}
}
二、Quartz
2.1 主要接口
-
Scheduler
Quartz实现任务调用最主要的接口,它包含
JobDetail
和Trigger
的注册和调度器的执行 -
SchedulerFactory
提供获取
Scheduler
的工厂接口,一般使用quartz提供实现类StdSchedulerFactory
-
Trigger
触发器接口,和Job执行相关,包含触发开始时间,结束时间, 下次开始时间等信息
-
SimpleTrigger
给定时间间隔触发,还可以指定次数
-
CronTrigger
cron表达式触发
-
CalendarIntervalTrigger
和
SimpleTrigger
类似,间隔时间触发,不同的是SimpleTrigger
间隔时间是毫秒,不能指定每个月(毫秒不固定)触发,CalendarIntervalTrigger
可以根据日历单位为时间间隔触发 -
DailyTimeIntervalTrigger
可以指定每天的某个时间段内,以一定的时间间隔执行任务,支持指定星期
-
-
JobDetail
描述Job实例详细属性的接口
-
Job
定时任务所要实现的接口
2.2 主要builder
-
ScheduleBuilder
-
SimpleScheduleBuilder
// 每隔1s执行,重复10次 SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(1) .withRepeatCount(10);
-
CronScheduleBuilder
// 每分钟0 10 .. 50秒执行 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?");
-
CalendarIntervalScheduleBuilder
// 每隔1个月执行1次 CalendarIntervalScheduleBuilder cbuilder = CalendarIntervalScheduleBuilder.calendarIntervalSchedule() .withIntervalInMonths(1);
-
DailyTimeIntervalScheduleBuilder
// 每天9点半到18点半,每隔30秒执行一次 DailyTimeIntervalScheduleBuilder dailyBuilder = DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule() .withIntervalInSeconds(30) .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 30)) .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 30));
-
-
JobBuilder
JobDetail jobDetail = JobBuilder.newJob(CustomJob.class).withIdentity("job1", "group1").build();
-
TriggerBuilder
Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1") .startNow() .withSchedule(simpleScheduleBuilder).build();
实际执行次数=重复次数+1,重复次数和结束时间冲突看谁先结束
-
示例
public class CustomJob implements Job { private static final Logger logger = LoggerFactory.getLogger(CustomJob.class); @Override public void execute(JobExecutionContext context) throws JobExecutionException { logger.info("-> invoke"); } }
SchedulerFactory factory = new StdSchedulerFactory(); Scheduler scheduler = factory.getScheduler(); SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(10) .withRepeatCount(1); CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?"); CalendarIntervalScheduleBuilder cbuilder = CalendarIntervalScheduleBuilder.calendarIntervalSchedule() .withIntervalInMonths(1); DailyTimeIntervalScheduleBuilder dailyBuilder = DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule() .withIntervalInSeconds(30) .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 30)) .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 30)); LocalDateTime now = LocalDateTime.now().plusMinutes(1); ZonedDateTime zonedDateTime = now.atZone(ZoneId.systemDefault()); Instant instant = zonedDateTime.toInstant(); Date newDate = Date.from(instant); JobDetail jobDetail = JobBuilder.newJob(CustomJob.class).withIdentity("job1", "group1").build(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1") .startNow() .endAt(newDate) .withSchedule(simpleScheduleBuilder).build(); scheduler.scheduleJob(jobDetail, trigger); scheduler.start();
2.3 cron表达式
cron表达式是由六个或七个子表达式(字段)组成的字符串,用于描述计划的各个详细信息
秒 分 时 日 月 周 年
字段名字 | 是否必填 | 允许值 | 允许特殊字符 |
---|---|---|---|
秒 | Y | 0-59 | , - * / |
分 | Y | 0-59 | , - * / |
时 | Y | 0-23 | , - * / |
日 | Y | 1-31 | , - * / ? L W C |
月 | Y | 0-11 或 JAN-DEC | , - * / |
周 | Y | 1-7 或 SUN-SAT | , - * / ? L C # |
年 | N | 空或 1970-2099 | , - * / |
特殊字符说明
*
: 代表所有,每个值。例如 *在秒字段,代表每秒?
: 代表无具体值。常用于周和日上,选定某日,不在乎是否周几或者选定周几,不在乎某日。-
用于指定范围。例如1-3 在小时字段代表1,2,3小时,
用于指定一些值。例如1,2,4 在小时字段代表1,2,4小时/
用于指定增量。例如 0/15 在分钟字段代表第0,15,30,45分钟,5/15代表第5,20,35,50分钟L
代表最后一个,在日和周两个字段中,单独使用L,在日中代表月的最后一日,在周中代表周六。如果加上数字,例如6L在周字段上,代表月最后一个星期五。也可以用L-3在日字段上表示月的倒数第三天。W
用于指定最接近的工作日(周一到周五),只能用于日字段。指定的工作日的范围是当前搜索的月,W不能用户日期范围或日期列表。例如:15W 在日字段表示距离15日最近的工作日#
用于指定月第几个周几,只能用于日字段。例如6#3 代表月第三个周五,如果日期不存在则不会触发
例子
0 0 12 * * ?
每天12点触发0 0 10-20 * * ?
每天10点到20点 整点触发0 0 10,15,20 * * ?
每天10,15,20点 整点触发0 */10 * * *?
每10分钟触发0 0/10 * * * ?
每小时 0,10,20,30,40,50分时触发0 30 17 25 * ?
每月25号17点30触发0 30 17 25W * ?
每月距离25日最近的工作日 17点30分触发0 30 17 LW * ?
每月最后一个工作日17点30分触发0 30 17 ? * 6L
每月最后礼拜五17点30分触发
三、分布式锁
多机部署实例时,定时任务也会同步执行,涉及数据库操作时,会发生数据库重复写,发生不可预知的错误,这时希望一个任务只有一台机器执行。分布式锁可以解决这种问题。针对这种情况,可以分为以下两种.
3.1 Spring Scheduled
-
如果是简单的定时任务,直接使用@Scheduled注解实现的定时任务,可以使用shedlock这个轻量级框架。官方地址:https://github.com/lukas-krecan/ShedLock
-
使用自定义定时任务,而不是使用注解,可以考虑redis,使用setnx实现分布式锁功能
3.2 Quartz
TriggerListener
接口的实现类可以监听触发器执行任务,可以在执行之前判断是否能拿到分布式锁,然后判断是否执行。- 分布式锁的实现方式有很多:常见三种:数据库行锁,redis根据setnx实现锁,和zookeeper临时顺序节点实现锁。三种方式各有优缺点,可根据项目情况选择。