Java基于Spring的TaskScheduler做定时任务,对任务可以实时增删改查

开发环境

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);
    }
}

业务代码动态使用定时任务

任务要持久化(项目启动就要加载需要执行的任务)

  1. 可以项目启动完成自动监听ApplicationReadyEvent ,
  2. 然后去查询数据库表的任务,丢给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);
        }
    }

运行查看效果

在这里插入图片描述

OK 完结撒花。。。。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值