探索動(dòng)態(tài)執(zhí)行的計(jì)劃任務(wù)-DynamicSchedule
背景
在現(xiàn)代軟件開發(fā)中,計(jì)劃任務(wù)是一種常見的需求。無論是定時(shí)發(fā)送郵件、定期清理緩存,還是執(zhí)行數(shù)據(jù)同步,計(jì)劃任務(wù)都能幫助我們自動(dòng)化這些重復(fù)性工作。
最近有一個(gè)需求,用戶想要自己設(shè)定定時(shí)時(shí)間,來動(dòng)態(tài)的執(zhí)行定時(shí)任務(wù)。 很離譜,原來每天晚上12點(diǎn)定時(shí)執(zhí)行的幾個(gè)數(shù)據(jù)同步、數(shù)據(jù)清理任務(wù),想不通用戶要這個(gè)功能干啥!?。?/p>
探索歷程
原本的cron表達(dá)式,是直接寫死到代碼里的,顯然不能動(dòng)態(tài)的修改。
如果采用配置文件的方式,每次改動(dòng)要重啟項(xiàng)目,或者再寫個(gè)定時(shí)任務(wù),每秒讀取文件內(nèi)容,也不太合適。
如果引入分布式任務(wù)調(diào)度平臺(tái),比如xxl-job、power-job、snail-job,又覺得太復(fù)雜。
選擇采用放到數(shù)據(jù)庫(kù)的方式,實(shí)現(xiàn)過程中,發(fā)現(xiàn)并不是很順利,寫一篇文章記錄一下這次的過程。
原本的實(shí)現(xiàn)
@Scheduled(cron = "0/5 * * * * *")
public void demo() {
System.out.println(LocalDateTime.now());
}
結(jié)果
圖片
動(dòng)態(tài)設(shè)置
配置類
@Component
@RequiredArgsConstructor
public class JobConfig implements SchedulingConfigurer {
private final ITestJobService jobService;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(
//1.添加任務(wù)內(nèi)容(Runnable)
() -> System.out.println("執(zhí)行動(dòng)態(tài)定時(shí)任務(wù)1: " + LocalDateTime.now()),
//2.設(shè)置執(zhí)行周期(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("修改時(shí)間:"+ LocalDateTime.now());
return "success";
}
將 0/10 * * * * * 改為 0/5 * * * * *
結(jié)果
圖片
可以看出來 修改的時(shí)間是 15:01 ,但是下次執(zhí)行時(shí)間還是間隔了10秒,第二次之后的時(shí)間才是間隔5秒。 更新結(jié)果有一個(gè)周期的延遲。
在這種情況下,延遲還算可以接收,但是周期如果是一天、一周,那生效周期就太長(zhǎng)了,需要一種即時(shí)生效的方法。
即時(shí)生效
實(shí)現(xiàn)方案是,以事件驅(qū)動(dòng),動(dòng)態(tài)修改定時(shí)任務(wù)。
定義事件
@Getter
public class ScheduleTaskUpdateEvent extends ApplicationEvent {
private final Integer taskId;
public ScheduleTaskUpdateEvent(Object source, Integer taskId) {
super(source);
this.taskId = taskId;
}
}
構(gòu)造調(diào)度任務(wù)程序
@Configuration
public class SchedulerConfig {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 設(shè)置線程池大小
scheduler.setThreadNamePrefix("scheduled-task-");
scheduler.initialize();
return scheduler;
}
}
動(dòng)態(tài)任務(wù)配置
@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);
});
}
// 根據(jù)任務(wù)配置創(chuàng)建任務(wù)
private ScheduledFuture<?> scheduleTask(TestJob job) {
System.out.println("創(chuàng)建新的定時(shí)任務(wù),id:" + job.getId() + ", cron: " + job.getCron());
return taskScheduler.schedule(
() -> System.out.println("執(zhí)行動(dòng)態(tài)定時(shí)任務(wù)2: " + LocalDateTime.now()),
triggerContext -> {
return new CronTrigger(job.getCron()).nextExecutionTime(triggerContext).toInstant();
}
);
}
@Override
public void onApplicationEvent(ScheduleTaskUpdateEvent event) {
System.out.println("收到修改定時(shí)任務(wù)事件,任務(wù)id:" + event.getTaskId());
// 取消并移除舊任務(wù)
ScheduledFuture<?> future = scheduledTasks.get(event.getTaskId());
if (future != null) {
future.cancel(false);
scheduledTasks.remove(event.getTaskId());
}
// 獲取最新的任務(wù)配置并重新注冊(cè)該任務(wù)
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("修改時(shí)間:"+ LocalDateTime.now());
return "success";
}
結(jié)果
圖片
可以看到,在收到修改任務(wù)的事件后,直接刪除了原來的定時(shí)任務(wù),創(chuàng)建了一個(gè)新的執(zhí)行任務(wù),即時(shí)生效,不需要等待一個(gè)執(zhí)行周期就可立即執(zhí)行。
小結(jié)
通過上述方法,我們可以在 Spring Boot 應(yīng)用中實(shí)現(xiàn)動(dòng)態(tài)計(jì)劃任務(wù),使得任務(wù)的執(zhí)行更加靈活可控。
還實(shí)驗(yàn)了幾種不同的方式,比如每秒輪詢數(shù)據(jù)庫(kù)、手動(dòng)計(jì)算cron表達(dá)式 的執(zhí)行時(shí)間。感覺就屬這個(gè)事件驅(qū)動(dòng)的方式最優(yōu)雅。