大家好,我是程序员牛肉。
相信大家在学习苍穹外卖的时候,就使用过定时任务。但是那个时候我们使用的定时任务的方式还比较死板:
[它没有办法进行动态的修改,每一次我们想要停止或者修改这个定时任务的定时机制,就要停止这个任务,在代码中手动进行修改之后,重新启动项目。]
可是对于大型项目而言,这种停止再启动的操作实在是太耗费时间了。
因此我们今天学习一个更加高端的方式:可以实现在不停机的情况下,动态的控制这些定时任务。
在正式介绍方法之前,我们要先学习一个类:ThreadPoolTaskScheduler。
[ThreadPoolTaskScheduler 是 Spring 框架中的一个重要组件,它实现了 TaskScheduler 接口,提供了基于线程池的任务调度和执行功能。这个组件非常适合用于需要在应用程序中创建和管理多个并发任务的场景。]
用更加通俗的话来讲:我们可以认为这是一个多线程下的定时任务调度器,它允许我们将定时任务像线程一样的进行提交执行之后,通过返回类来控制定时任务。
当我们把一个定时任务提交给他之后,他的返回类名称叫做ScheduledFuture,我们可以对这个返回类进行以下操作:
在了解完这两个类之后,相信我们已经大概有设计思想了:其实就是将定时任务提交给ThreadPoolTaskScheduler,获取到这个定时任务的控制类ScheduledFuture。之后就可以进行各种操作了。
接下来我们看一看在Spring Boot中到底应该如何实现:
首先,我们需要写一个配置类,表明这个项目中的定时任务采用ThreadPoolTaskScheduler 来调度和执行定时任务:
@SpringBootConfiguration
public class ScheduleConfig {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
return taskScheduler;
}
}
有了这个配置类之后,我们还需要写一个定时任务接口。让定时任务这个接口引用Runnable这个接口。表示定时任务可以被线程化执行。
public interface ScheduleTask extends Runnable {
/**
* 获取定时任务的名称
*
* @return
*/
String getName();
}
除此之外,我们还需要自定义一个类:将线程任务scheduleTask和其对应的返回类ScheduledFuture绑定起来。因为我们仅通过ScheduledFuture并不能知晓当前操作的是哪个定时任务,不方便打日志。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ScheduleTaskHolder implements Serializable {
/**
* 执行任务实体
*/
private ScheduleTask scheduleTask;
/**
* 执行任务的结果实体
*/
private ScheduledFuture scheduledFuture;
}
在完成这三个基础类之后。其实设计思路就更加清晰了。
[我们需要创建一个键值对类型的数据结构。键是UUID,用来作为唯一标识符。值是ScheduleTaskHolder。当我们将多个定时任务传递给ThreadPoolTaskScheduler后,使用键值对类型的数据结构进行存储。再将UUID返回。后续可以根据UUID获取到对应的ScheduleTaskHolder,从ScheduleTaskHolder中获取ScheduledFuture进行各种操作。]
这样我们就实现了定时任务的动态启停。而在上述我们提到了:我们需要一个键值对类型的数据结构。
那么我们要选取哪一个键值对数据结构来存储信息呢?
我们可以想一想,由于这个键值对数据结构可以被多个线程操纵;比如可能有多个管理员同时修改定时任务。那么我们对这个键值对数据结构的基本要求就应该是:能够保证数据安全。
到底选取java哪一个集合呼之欲出:ConcurrentHashMap。
[ConcurrentHashMap 是 Java 并发包 java.util.concurrent 中的一个线程安全的哈希表实现。它是 Java 集合框架中的一个非常重要的部分,特别是在多线程环境中。ConcurrentHashMap 通过锁分段技术(在 JDK 1.7 版本中)或者使用 CAS(Compare-And-Swap)操作和 synchronized 关键字(在 JDK 1.8 版本中)来保证线程安全,允许多个线程可以同时读写不同的段。]
而我们的项目中确实也是使用ConcurrentHashMap来存储信息的:
所有的准备工作都介绍完了,让我们直接看看如何实现不停机自由修改定时任务:
1.尝试启动一个任务:
这里的逻辑是:把scheduletask和对应的cron表达式提交给ThreadPoolTaskScheduler。之后将scheduletask以及对应的ScheduledFuture封装成为ScheduleTaskHolder。使用UUID生成一个唯一标识符,将UUID和ScheduleTaskHolder作为键值对存储到ConcurrentHashMap中。返回key。
public String startTask(ScheduleTask scheduleTask, String cron) {
//将任务提交给 ScheduledFuture ,这个类可以用来查询当前任务的状态,可以用来取消或者修改任务
ScheduledFuture<?> scheduledFuture = taskScheduler.schedule(scheduleTask, new CronTrigger(cron));
//生成当前任务的UUID
String key = UUIDUtil.getUUID();
//用于存储任务的执行逻辑和调度信息。
ScheduleTaskHolder holder = new ScheduleTaskHolder(scheduleTask, scheduledFuture);
//将他放到缓存中
cache.put(key, holder);
log.info("{} 启动成功!唯一标识为:{}", scheduleTask.getName(), key);
return key;
}
2.尝试停止一个任务:
这里的逻辑是:根据传入的key,从ConcurrentHashMap中获取到对应的ScheduleTaskHolder。从ScheduleTaskHolder中拿到ScheduledFuture。调用ScheduledFuture的cancle()方法。
public void stopTask(String key) {
//检查是否为空
if (StringUtils.isBlank(key)) {
return;
}
//从cache从拿到对应的定时任务
ScheduleTaskHolder holder = cache.get(key);
//如果这个定时任务为空,就返回
if (Objects.isNull(holder)) {
return;
}
//从holder中拿到对应的scheduledFuture
ScheduledFuture scheduledFuture = holder.getScheduledFuture();
//设置它的停止表示为true。获取返回状态
boolean cancel = scheduledFuture.cancel(true);
if (cancel) {
log.info("{} 停止成功!唯一标识为:{}", holder.getScheduleTask().getName(), key);
} else {
log.error("{} 停止失败!唯一标识为:{}", holder.getScheduleTask().getName(), key);
}
}
3.尝试修改一个定时任务的定时:
这里的逻辑是:根据传入的key获取到对应的根据传入的key,从ConcurrentHashMap中获取到对应的ScheduleTaskHolder。从ScheduleTaskHolder中拿到ScheduledFuture,从ScheduleTaskHolder获取到scheduletask。
使用ScheduledFuture取消当前任务。使用scheduletask以及传入的新的cron表达式调用startTask()重新开启一个新的定时任务。
public String changeTask(String key, String cron) {
//检查输入进来的key和cron
if (StringUtils.isAnyBlank(key, cron)) {
throw new RPanFrameworkException("定时任务的唯一标识以及新的执行表达式不能为空");
}
//检查获取到的缓存
ScheduleTaskHolder holder = cache.get(key);
if (Objects.isNull(holder)) {
throw new RPanFrameworkException(key + "唯一标识不存在");
}
//停止当前的定时任务
stopTask(key);
//根据新cron重新开启一个定时任务
return startTask(holder.getScheduleTask(), cron);
}
其实这也能解释为什么我们要将scheduletask和ScheduledFuture封装成为一个ScheduleTaskHolder。
因为我们在修改任务定时的时候,需要获取到scheduletask。这样才可以调用startTask来重新开启一个定时任务。
关于动态修改定时任务的方案我就介绍到这里。希望我的文章可以帮到你。
对于这种方案,你有什么看法呢?