开发环境
windows + idea2024 + JDK21 + Maven + Springboot3.4
配置Spring的TaskScheduler任务线程池
JDK21可以使用虚拟线程池优化传统的线程池
/**
* 万里悲秋常作客,百年多病独登台
*
* @author : makeJava
*/
@Configuration
public class hotWorldSchedulingConfig implements SchedulingConfigurer {
/**
* 使用虚拟线程优化任务池
*
* @return ThreadPoolTaskScheduler
*/
@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
// 设置虚拟线程工厂
scheduler.setThreadFactory(Thread.ofVirtual().name("vir-scheduler-").factory());
// 设置核心线程数,根据需求调整
scheduler.setPoolSize(4);
scheduler.initialize();
return scheduler;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler taskScheduler = threadPoolTaskScheduler();
taskRegistrar.setTaskScheduler(taskScheduler);
}
}
小于JDK21使用传统的ThreadPoolTaskScheduler线程池
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
/**
* 万里悲秋常作客,百年多病独登台
*
* @author : makeJava
*/
@Configuration
public class hotWorldSchedulingConfig implements SchedulingConfigurer {
/**
* 使用虚拟线程优化任务池
*
* @return ThreadPoolTaskScheduler
*/
@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
// 创建一个线程池调度器
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
// 设置线程池大小
taskScheduler.setPoolSize(4);
// 设置线程名前缀
taskScheduler.setThreadNamePrefix("scheduled-hot-world-pool-");
// 设置等待任务完成再关闭线程池
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间(单位:秒)
taskScheduler.setAwaitTerminationSeconds(60);
taskScheduler.initialize();
return taskScheduler;
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
// 设置虚拟线程工厂
scheduler.setThreadFactory(Thread.ofVirtual().name("vir-scheduler-").factory());
// 设置核心线程数,根据需求调整
scheduler.setPoolSize(4);
scheduler.initialize();
return scheduler;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler taskScheduler = threadPoolTaskScheduler();
taskRegistrar.setTaskScheduler(taskScheduler);
}
}
编写ThreadPoolTaskScheduler的执行任务的Service{对任务可以增删改查}
Spring的定时任务通过开启注解,使用注解很灵活 @Scheduled(fixedRate = 1000) // 每隔 1 秒执行一次
但是有缺陷,就是无法动态修改,这玩意就是写死在代码里的,没办法动态修改或者停止这个任务
我们想要动态添加任务,修改任务,延迟执行,删除任务,不懂代码的情况下完美适配
有很多的第三方方案,什么quartz,xxl-job等等很多job框架,我们要最轻量级不引入任何第三方解决
(一)核心{addOrUpdateTask}添加新的任务或者更新任务
(二)核心{cancelTask}移除任务
(三)核心{lazyLoadingTask}延迟加载任务
(四)核心{getAllTaskIds}获取目前正在运行的任务
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
/**
* 万里悲秋常作客,百年多病独登台
*
* @author : makeJava
*/
@Component
@Slf4j
public class HotWorldDynamicTaskService {
@Autowired
private ThreadPoolTaskScheduler taskScheduler;
/**、
* 存储任务Future的Map
*/
private final Map<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
/**
* 添加一个新的定时任务||更新已存在的定时任务
* @param taskId 任务ID
* @param cronExpression cron表达式
* @param task 任务
*/
public void addOrUpdateCronTask(String taskId, String cronExpression, Runnable task) {
log.info("加载定时任务----Id--{}||表达式---->{}", taskId, cronExpression);
// 验证cronExpression是否有效
if (taskId == null || taskId.trim().isEmpty()) {
throw new IllegalArgumentException("任务ID不能为空");
}
try {
cronExpression = convertQuartzToSpringBootCron(cronExpression);
// 检查cron表达式的合法性
if (!CronExpression.isValidExpression(cronExpression)) {
throw new IllegalArgumentException("无效的cron表达式: " + cronExpression);
}
} catch (Exception e) {
throw new IllegalArgumentException("cron表达式错误: " + e.getMessage(), e);
}
// 如果任务已存在,先移除
if (scheduledTasks.containsKey(taskId)) {
cancelTask(taskId);
}
// 创建触发器
CronTrigger trigger = new CronTrigger(cronExpression);
// 调度任务并保存future
ScheduledFuture<?> future = taskScheduler.schedule(task, trigger);
scheduledTasks.put(taskId, future);
}
/**
* 转换 Quartz Cron 表达式为 Spring Boot Cron 表达式
*
* @param quartzCron Quartz Cron 表达式
* @return Spring Boot Cron 表达式
*/
public static String convertQuartzToSpringBootCron(String quartzCron) {
// 将 Quartz Cron 表达式按照空格分割成数组
String[] quartzCronParts = quartzCron.trim().split(" ");
// 如果 Quartz Cron 表达式有 7 个字段,去掉第一个字段 (年字段)
if (quartzCronParts.length == 7) {
// 删除年字段,并返回前六个字段作为 Spring Boot Cron 表达式
return String.join(" ", quartzCronParts[0], quartzCronParts[1], quartzCronParts[2], quartzCronParts[3], quartzCronParts[4], quartzCronParts[5]);
}
// 如果是 Spring Boot 格式的 6 字段 Cron 表达式,直接返回
if (quartzCronParts.length == 6) {
return String.join(" ", quartzCronParts);
}
// 如果格式不符合标准,抛出异常
throw new IllegalArgumentException("Invalid Cron expression format.");
}
/**
* 取消任务
* @param taskId 任务id
* @return boolean
*/
public boolean cancelTask(String taskId) {
ScheduledFuture<?> future = scheduledTasks.get(taskId);
if (future != null) {
boolean cancelled = future.cancel(true);
if (cancelled) {
scheduledTasks.remove(taskId);
}
return cancelled;
}
return false;
}
/**
* 获取所有任务ID
* @return Set<String>
*/
public Set<String> getAllTaskIds() {
return scheduledTasks.keySet();
}
/**
* 延迟执行
*/
public void lazyLoadingTask(Date delayedStartTime, Runnable taskRunner) {
// 设置任务执行的时间点
Instant startTime = delayedStartTime.toInstant();
taskScheduler.schedule(taskRunner, startTime);
}
}
业务代码动态使用定时任务
任务要持久化(项目启动就要加载需要执行的任务)
- 可以项目启动完成自动监听ApplicationReadyEvent ,
- 然后去查询数据库表的任务,丢给Spring的TaskScheduler做定时任务跑起来
动态添加任务和销毁任务需要业务判断
判断任务规则丢过来的是否没有使用任务,或者要销毁任务
判断任务结束时间是否已经小于当前时间了,任务不能在运行了
判断任务的开始时间是否是未达到,需要延时执行
被执行的任务自己做好多节点的轮替{或者串行避免重复执行}|最好使用取模方式各司其职最大化分压和性能提升
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
/**
* 万里悲秋常作客,百年多病独登台
*
* @author : makeJava
*/
@Component
@Slf4j
@SuppressWarnings("all")
public class TaskCronProxyExecutor {
@Resource
private ScheduleManager scheduleManager;
@Autowired
private HotWorldDynamicTaskService hotWorldDynamicTaskService;
/**
* 执行定时任务---代理新增
* 2025-04-29 19:26:00 [scheduled-hot-world-pool-4] INFO .b.h.s.i.HotWorldTemplateServiceImpl:409 -
* 增量---定时任务--触发---PDD-数据集-热词库-数据-插入-ES执行开始....1909181628183592962
* <p>
* 2025-04-29 19:28:00 [scheduled-hot-world-pool-2] INFO .b.h.s.i.HotWorldTemplateServiceImpl:409
* - 增量---定时任务--触发---PDD-数据集-热词库-数据-插入-ES执行开始....1909181628183592962
*/
public void executeBySpringTask(HotWorldTemplate hotWorldTemplate) {
String hotWorldTemplateName = hotWorldTemplate.getName();
String taskId = hotWorldTemplate.getId().toString();
String cronText = hotWorldTemplate.getSchedulerCronText();
// 如果关闭了定时任务
if (!hotWorldTemplate.getSchedulerEnable()) {
hotWorldDynamicTaskService.cancelTask(taskId);
log.info("任务关闭---取消热词任务----->{}", hotWorldTemplateName);
return;
}
// 如果任务到期了,也不执行
if (hotWorldTemplate.getSchedulerCronEnd() != null && hotWorldTemplate.getSchedulerCronEnd().before(new Date())) {
hotWorldDynamicTaskService.cancelTask(taskId);
log.info("任务到期---取消热词任务----->{}", hotWorldTemplateName);
return;
}
// 任务开始时间--延迟执行
if (hotWorldTemplate.getSchedulerCronBegin() != null && hotWorldTemplate.getSchedulerCronBegin().after(new Date())) {
log.info("任务延迟开启---热词任务----->{}", hotWorldTemplateName);
hotWorldDynamicTaskService.lazyLoadingTask(hotWorldTemplate.getSchedulerCronBegin(), new Runnable() {
@Override
public void run() {
executeHotWorldTaskHandler(taskId, cronText, hotWorldTemplateName);
}
});
return;
}
executeHotWorldTaskHandler(taskId, cronText, hotWorldTemplateName);
}
public void executeHotWorldTaskHandler(String taskId, String schedulerCronText, String hotWorldTemplateName) {
log.info("加载定时任务---热词分析----begin-ing-------{}", hotWorldTemplateName);
hotWorldDynamicTaskService.addOrUpdateCronTask(taskId,
schedulerCronText, new Runnable() {
@Override
public void run() {
try {
HotWorldTemplateServiceImpl templateService = SpringUtils.getBean(HotWorldTemplateServiceImpl.class);
templateService.startBySpringCoreScheduling(taskId);
} catch (Exception e) {
log.error("{}---热词分析任务执行失败", hotWorldTemplateName, e);
}
}
});
log.info("加载定时任务---热词分析----ending-ing-------{}", hotWorldTemplateName);
}
/**
* 执行定时任务---代理取消
*/
public boolean delCoreBySpringTask(String taskId) {
return hotWorldDynamicTaskService.cancelTask(taskId);
}
/**
* 执行定时任务---项目启动调用
*/
public void initHotWorldTask() {
log.info("项目启动完成加载热词定时任务.....");
HotWorldTemplateServiceImpl templateService = SpringUtils.getBean(HotWorldTemplateServiceImpl.class);
List<HotWorldTemplate> list = templateService.listByTaskForJob();
for (HotWorldTemplate hotWorldTemplate : list) {
executeBySpringTask(hotWorldTemplate);
}
}
}
被执行的任务,这里采用分布式锁,保证唯一执行
你可以使用取模计算当前节点是否执行改task
下面代码示例有所删减…参考逻辑就行
/**
* 定时任务--增量数据刷盘ES
*
* @param taskId 任务热词ID
* @return null
*/
@Override
public Map<String, Long> startBySpringCoreScheduling(String taskId) {
// 防止多接点重复重复执行
boolean ifAbsent = RedisUtils.setObjectIfAbsent("hotWorldTask_Scheduler:" + taskId, "1", Duration.ofMillis(10));
if (!ifAbsent) {
return null;
}
Date schedulerCronEnd = hotWorldTemplate.getSchedulerCronEnd();
if (schedulerCronEnd != null && schedulerCronEnd.before(new Date())) {
log.warn("热词-任务已过期--->{}", taskId);
boolean delCoreBySpringTask = taskCronProxyExecutor.delCoreBySpringTask(taskId);
log.warn("热词-任务已过期--删除任务状态--{}", delCoreBySpringTask);
return null;
}
String fieldConfig = hotWorldTemplate.getFieldConfig();
DatasetGroupInfoDTO datasetGroupInfoDTO = JSONUtil.toBean(fieldConfig, DatasetGroupInfoDTO.class);
try {
String hotWorldField = hotWorldTemplate.getHotWorldField();
String worldFilterTime = hotWorldTemplate.getWorldFilterTime();
// 全量跑-数据
int offset = 0;
int offsetLimit = 1000;
// 增量限制最高五百轮--50w的数据
int maxIterations = 500;
// 当前自增的ID--自增字段值
Object autoPrimaryKey = null;
autoPrimaryKey = hotWorldTemplate.getLastDataId();
log.info("{}定时任务触发------当前自增的ID--自增字段值:{}", hotWorldTemplate.getName(), autoPrimaryKey);
// 时间最小颗粒度:0单日|1周|2月...'
Map<String, String> beginDateAndEndDate = new HashMap<>();
// 只有不走主键自增索引的字段会使用时间去走定时任务
if (autoPrimaryKey == null) {
beginDateAndEndDate = buildDateBetween(hotWorldTemplate);
}
/* 批处理循环执行---查询数据源数据导入Es中*/
while (true) {
// 每次查询数据源数据--最大限制500轮{为什么设置限制,防止客户数据库每时每秒都有数据刷入,这样我们的增量永远死循环跑}
maxIterations--;
if (maxIterations <= 0) {
log.info("{}定时任务触发------增量数据刷盘ES--数据量超过最大限制50w,本次终止执行", hotWorldTemplate.getName());
break;
}
// 如果热词数据库导入---走的是自增主键索引,百万数据的定时任务就是最后执行的数据Id,到目前加载完所有剩余数据
Map<String, Object> dataWithLimit = dataSourceEnginProxy.previewDataWithLimit(datasetGroupInfoDTO, offset, offsetLimit,
false, originName, beginDateAndEndDate.get("beginDate"), beginDateAndEndDate.get("endOffDate"), limitField, autoPrimaryKey);
Object data = dataWithLimit.get("data");
JSONObject entries = JSONUtil.parseObj(data);
if (entries.isEmpty()) {
log.info("{}-热词库无法找到有效数据,循环结束", hotWorldTemplate.getName());
break;
}
// 字段Id-翻译为结果集的有效代码字符
String endTimeKey = "";
String endAutoPrimaryKey = "";
List<String> everyField = new ArrayList<>();
List<JSONObject> fieldsDataList = entries.getBeanList("fields", JSONObject.class);
checkDateOfEnd(hotWorldTemplate, nextData, endTimeKey);
// 调用ES保存数据 --> 获取数据丢入分词器
esQueryProxyManage.bulkSaveToElasticsearch(hotWordList, taskId);
offset += offsetLimit;
}
// 更新最近的一次日期
HotWorldTemplate timeUpdate = new HotWorldTemplate();
hotWorldTemplateMapper.updateById(timeUpdate);
log.info("增量---定时任务--触发---{}-热词库-数据-插入-ES执行完毕", hotWorldTemplate.getName());
// 全量热词
return null;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException("热词分析任务启动失败");
} finally {
RedisUtils.deleteObject("hotWorldTask_Scheduler:" + taskId);
}
}