背景
在现代软件开发中,计划任务是一种常见的需求。无论是定时发送邮件、定期清理缓存,还是执行数据同步,计划任务都能帮助我们自动化这些重复性工作。
最近有一个需求,用户想要自己设定定时时间,来动态的执行定时任务。 很离谱,原来每天晚上12点定时执行的几个数据同步、数据清理任务,想不通用户要这个功能干啥!!!
探索历程
原本的cron表达式,是直接写死到代码里的,显然不能动态的修改。
如果采用配置文件的方式,每次改动要重启项目,或者再写个定时任务,每秒读取文件内容,也不太合适。
如果引入分布式任务调度平台,比如xxl-job、power-job、snail-job,又觉得太复杂。
选择采用放到数据库的方式,实现过程中,发现并不是很顺利,写一篇文章记录一下这次的过程。
原本的实现
@Scheduled(cron = "0/5 * * * * *")
public void demo() {
System.out.println(LocalDateTime.now());
}
结果
图片
动态设置
配置类
@Component
@RequiredArgsConstructor
public class JobConfig implements SchedulingConfigurer {
private final ITestJobService jobService;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(
//1.添加任务内容(Runnable)
() -> System.out.println("执行动态定时任务1: " + LocalDateTime.now()),
//2.设置执行周期(Trigger)
triggerContext -> {
TestJob job = jobService.getById(1L);
return new CronTrigger(job.getCron()).nextExecutionTime(triggerContext).toInstant();
}
);
}
}
修改入口
@GetMapping("upd")
public String upd(@RequestParam("cron") String cron) {
jobService.updateById(new TestJob(1, cron));
System.out.println("修改时间:"+ LocalDateTime.now());
return "success";
}
将 0/10 * * * * * 改为 0/5 * * * * *
结果
图片
可以看出来 修改的时间是 15:01 ,但是下次执行时间还是间隔了10秒,第二次之后的时间才是间隔5秒。 更新结果有一个周期的延迟。
在这种情况下,延迟还算可以接收,但是周期如果是一天、一周,那生效周期就太长了,需要一种即时生效的方法。
即时生效
实现方案是,以事件驱动,动态修改定时任务。
定义事件
@Getter
public class ScheduleTaskUpdateEvent extends ApplicationEvent {
private final Integer taskId;
public ScheduleTaskUpdateEvent(Object source, Integer taskId) {
super(source);
this.taskId = taskId;
}
}
构造调度任务程序
@Configuration
public class SchedulerConfig {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 设置线程池大小
scheduler.setThreadNamePrefix("scheduled-task-");
scheduler.initialize();
return scheduler;
}
}
动态任务配置
@Component
public class DynamicScheduleTaskConfig implements ApplicationListener<ScheduleTaskUpdateEvent> {
@Resource
private ITestJobService jobService;
@Resource
private TaskScheduler taskScheduler;
private final Map<Integer, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
@PostConstruct
private void initializeTasks() {
List<TestJob> list = jobService.list();
list.forEach(job -> {
ScheduledFuture<?> future = scheduleTask(job);
scheduledTasks.put(job.getId(), future);
});
}
// 根据任务配置创建任务
private ScheduledFuture<?> scheduleTask(TestJob job) {
System.out.println("创建新的定时任务,id:" + job.getId() + ", cron: " + job.getCron());
return taskScheduler.schedule(
() -> System.out.println("执行动态定时任务2: " + LocalDateTime.now()),
triggerContext -> {
return new CronTrigger(job.getCron()).nextExecutionTime(triggerContext).toInstant();
}
);
}
@Override
public void onApplicationEvent(ScheduleTaskUpdateEvent event) {
System.out.println("收到修改定时任务事件,任务id:" + event.getTaskId());
// 取消并移除旧任务
ScheduledFuture<?> future = scheduledTasks.get(event.getTaskId());
if (future != null) {
future.cancel(false);
scheduledTasks.remove(event.getTaskId());
}
// 获取最新的任务配置并重新注册该任务
TestJob job = jobService.getById(event.getTaskId());
ScheduledFuture<?> newFuture = scheduleTask(job);
scheduledTasks.put(job.getId(), newFuture);
}
}
修改接口,增加事件
@GetMapping("upd")
public String upd(@RequestParam("cron") String cron) {
jobService.updateById(new TestJob(1, cron));
eventPublisher.publishEvent(new ScheduleTaskUpdateEvent(this, 1));
System.out.println("修改时间:"+ LocalDateTime.now());
return "success";
}
结果
图片
可以看到,在收到修改任务的事件后,直接删除了原来的定时任务,创建了一个新的执行任务,即时生效,不需要等待一个执行周期就可立即执行。
小结
通过上述方法,我们可以在 Spring Boot 应用中实现动态计划任务,使得任务的执行更加灵活可控。
还实验了几种不同的方式,比如每秒轮询数据库、手动计算cron表达式 的执行时间。感觉就属这个事件驱动的方式最优雅。