探索动态执行的计划任务-DynamicSchedule

createh52周前 (03-27)技术教程2

背景

在现代软件开发中,计划任务是一种常见的需求。无论是定时发送邮件、定期清理缓存,还是执行数据同步,计划任务都能帮助我们自动化这些重复性工作。

最近有一个需求,用户想要自己设定定时时间,来动态的执行定时任务。 很离谱,原来每天晚上12点定时执行的几个数据同步、数据清理任务,想不通用户要这个功能干啥!!!

探索历程

原本的cron表达式,是直接写死到代码里的,显然不能动态的修改。

如果采用配置文件的方式,每次改动要重启项目,或者再写个定时任务,每秒读取文件内容,也不太合适。

如果引入分布式任务调度平台,比如xxl-job、power-job、snail-job,又觉得太复杂。

选择采用放到数据库的方式,实现过程中,发现并不是很顺利,写一篇文章记录一下这次的过程。

原本的实现

Bash
@Scheduled(cron = "0/5 * * * * *")  
public void demo() {  
    System.out.println(LocalDateTime.now());  
}

结果

image.png

动态设置

配置类

Bash
@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();  
                }  
        );  
    }  
}

修改入口

Bash
@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 * * * * *

结果

image.png

可以看出来 修改的时间是 15:01 ,但是下次执行时间还是间隔了10秒,第二次之后的时间才是间隔5秒。 更新结果有一个周期的延迟。

在这种情况下,延迟还算可以接收,但是周期如果是一天、一周,那生效周期就太长了,需要一种即时生效的方法。

即时生效

实现方案是,以事件驱动,动态修改定时任务。

  1. 定义事件
Bash
@Getter  
public class ScheduleTaskUpdateEvent extends ApplicationEvent {  
  
    private final Integer taskId;  
  
    public ScheduleTaskUpdateEvent(Object source, Integer taskId) {  
        super(source);  
        this.taskId = taskId;  
    }  
}
  1. 构造调度任务程序
Bash
@Configuration  
public class SchedulerConfig {  
  
    @Bean  
    public ThreadPoolTaskScheduler taskScheduler() {  
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();  
        scheduler.setPoolSize(10); // 设置线程池大小  
        scheduler.setThreadNamePrefix("scheduled-task-");  
        scheduler.initialize();  
        return scheduler;  
    }  
}
  1. 动态任务配置
Bash
@Component  
public class DynamicScheduleTaskConfig implements ApplicationListener {  
  
    @Resource  
    private ITestJobService jobService;  
  
    @Resource  
    private TaskScheduler taskScheduler;  
  
    private final Map<Integer, ScheduledFuture> scheduledTasks = new ConcurrentHashMap<>();  
  
    @PostConstruct  
    private void initializeTasks() {  
        List 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);  
    }  
}
  1. 修改接口,增加事件
Bash
@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表达式 的执行时间。感觉就属这个事件驱动的方式最优雅。

如果还有其他实现方式,可以在评论区交流交流。

相关文章

java + redis zset实现延迟队列(定时到期执行任务)

在Redis中,zet作为有序集合,可以利用其有序的特性,将任务添加到zset中,将任务的到期时间作为score,利用zset的默认有序特性,zrangewithscores可以获取score值最小的...

Spring Boot 架构下的订单自动取消机制:定时任务篇

引言在电子商务领域,确保交易流程的顺畅和高效至关重要。一个常见的场景是,用户生成订单后,系统会给予一定的支付时间窗口,如果在这个窗口内用户未完成支付,订单应当自动取消,以避免资源锁定和库存占用。本文将...

老板喊你设计一个高效的定时任务系统

【51CTO.com原创稿件】今天想跟大家一起探讨一个听起来很简单的话题:定时任务机制。图片来自 Pexels无非就是一个计时器,到了指定时间就开始跑呗。too young,要是这么简单我还说啥呢,干...