Spring定时任务@Scheduled的使用
说明
@Scheduled是spring自带的注解,默认是单线程,常用作定时任务使用,
但是如果是集群版的机器的话,就考虑加上 分布式锁 或者使用 分布式定时任务(Elasticjob或者xxl-job等)代替。
简单的使用
1、在启动类上添加注解 @EnableScheduling
@SpringBootApplication
@EnableScheduling
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
2、在一个Bean中(@Component,需要将写定时任务的类交给spring管理)编写一个public void无参数方法,然后加上注解 @Scheduled
@Component
public class OrderJob {
// @Scheduled(fixedDelay = 2000) //固定延迟:代表下一个任务的开始与上一个任务的结束间隔总是固定的时长(毫秒),而且总是会等上一个任务完成了,才会开启下一个任务
// @Scheduled(fixedRate = 2000) //固定频率:定频任务的特性是任务的执行的时间间隔总是一样的。比如每1000毫秒执行一次
// @Scheduled(initialDelay = 10_000, fixedRate = 2_000) //启动延迟10秒,并以2秒的间隔执行任务
@Scheduled(cron = "*/2 * * * * ?") //使用cron表达式:每隔2秒执行一次任务
public void checkOrder() throws InterruptedException {
System.out.println("====================start" + LocalTime.now());
Thread.sleep(1000);
System.out.println("====================end" + LocalTime.now());
System.out.println();
}
}
一些常用的cron表达式
每隔5秒执行一次:*/5 * * * * ?
每隔10分钟执行一次:0 */10 * * * ?
每天23点执行一次:0 0 23 * * ?
每天凌晨1点执行一次:0 0 1 * * ?
每月1号凌晨1点执行一次:0 0 1 1 * ?
每月最后一天23点执行一次:0 0 23 L * ?
每周星期天凌晨1点实行一次:0 0 1 ? * L
在26分、29分、33分执行一次:0 26,29,33 * * * ?
每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?
其他参数设置
默认使用服务器默认时区
可以设置为java.util.TimeZone中的zoneId, 每天零点执行@Scheduled(cron = "0 0 0 * * ?", zone = "Asia/Shanghai")
fixedDelay 固定延迟
@Scheduled(fixedDelay = 5000) //上一次执行完毕时间点之后5秒再执行
fixedDelayString
与 fixedDelay 意思相同,只是使用字符串的形式,唯一不同的是支持占位符
@Scheduled(fixedDelayString = "5000")
fixedRate 固定频率
@Scheduled(fixedRate = 5000) //上一次开始执行时间点之后5秒再执行(每隔5秒执行一次)
---但是要注意:如果任务的执行所需时间(比如10秒)超过了 fixedRate(5秒),
---那么会等该次任务结束后才会执行下一次任务(单线程 串行),即相当于每隔10秒执行一次了
fixedRateString
与 fixedRate 意思相同,只是使用字符串的形式,唯一不同的是支持占位符
initialDelay
@Scheduled(initialDelay=1000, fixedRate=5000) //第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次
initialDelayString
initialDelay 的字符串String格式,initialDelay 意思相同,只是使用字符串的形式,唯一不同的是支持占位符
可以把定时任务的配置放到配置文件中,根据实际情况要修改时,不需要重新编译代码(需重启服务)
@Component
public class OrderJob {
//ps:如果在配置文件中未读取task.cron,那么会以下面提供的表达式默认执行
@Scheduled(cron = "${task.cron:*/2 * * * * ?}") //每隔2秒执行一次任务
public void checkOrder(){
System.out.println("====================" + LocalDateTime.now());
}
}
优缺点
优点
简单,拆箱即用,一些单机任务的情况比较适合
缺点
单线程:如果有两个任务A和B,那么任务A要是阻塞了,任务B就无法执行。
不支持集群:为避免重复执行的问题
不支持生命周期统一管理:不重启服务情况下关闭,启动任务
不支持动态调整:不重启服务的情况下修改任务参数
不支持分片任务:处理有序数据时,多机器分片执行任务处理不同数据
无报警机制:任务失败之后没有报警机制
不支持失败重试:出现异常后任务中介,不能根据执行状态控制任务重新执行
任务数据统计难以统计:任务数据量大时,对于任务执行情况无法高效的统计执行情况
使用多线程
因为@Scheduled默认使用的是单线程,如果有两个任务A和B,那么任务A要是阻塞了,任务B就无法执行,如何解决?
需要实现SchedulingConfigurer接口,然后自定义线程池,这样凡是用到@Scheduled注解的都可以用该线程池
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executors;
/**
* 多线程执行定时任务
*/
@Configuration
//所有的定时任务都放在一个线程池中,定时任务启动时使用不同都线程。
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
//设定一个长度10的定时任务线程池
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
}
}
可以通过Thread.currentThread().getName()拿到线程名称,便于在日志中进行排查问题。
补充:动态修改定时规则
import gov.minhang.aqrcode.basic.utils.DateUtils;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import java.time.LocalTime;
import java.util.Date;
/**
* 动态修改定时任务cron参数
*/
@Component
@EnableScheduling
public class TestTask implements SchedulingConfigurer {
private static String cron = "*/2 * * * * ?";
/**
* 提供set方法,由外部根据不同条件而修改cron表达式的值;
*/
public static void setCron(String param) {
cron = param;
}
public static String getCron() {
return cron;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(
new Runnable() {
@Override
public void run() {
// 这里执行定时任务的业务逻辑
System.out.println(Thread.currentThread().getName() + " " + LocalTime.now() + " 定时任务的业务逻辑 ");
}
}, new Trigger() {
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
// 定时任务触发,可修改定时任务的执行周期
CronTrigger trigger = new CronTrigger(cron);
Date nextExecDate = trigger.nextExecutionTime(triggerContext);
System.out.println(Thread.currentThread().getName() + " " + LocalTime.now() + " 修改任务的执行周期 " + " cron: " + cron + " 下次执行时间:" + DateUtils.date2LocalDateTime(nextExecDate));
return nextExecDate;
}
}
);
}
}
controller中方法:
/**
* 修改定时任务的cron
*/
@GetMapping({"/updateTaskCron"})
public R<Boolean> updateTaskCron(@RequestParam String cron) {
TestTask.setCron(cron);
return R.success();
}
/**
* 查询定时任务的cron
*/
@GetMapping({"/queryTaskCron"})
public R<String> queryTaskCron() {
return R.success(TestTask.getCron());
}