• SpringBoot 动态多线程并发定时任务


    一、简介

    实现定时任务有多种方式:

    • Timer:jdk 中自带的一个定时调度类,可以简单的实现按某一频度进行任务执行。提供的功能比较单一,无法实现复杂的调度任务。
    • ScheduledExecutorService:也是 jdk 自带的一个基于线程池设计的定时任务类。其每个调度任务都会分配到线程池中的一个线程执行,所以其任务是并发执行的,互不影响。
    • Spring Task:Spring 提供的一个任务调度工具,支持注解和配置文件形式,支持 Cron 表达式,使用简单但功能强大。
    • Quartz:一款功能强大的任务调度器,可以实现较为复杂的调度功能,如每月一号执行、每天凌晨执行、每周五执行等等,还支持分布式调度,就是配置稍显复杂。

    使用 spring 自带的,继承 SchedulingConfigurer 的方式。

    源码地址:

    Gitee: https://gitee.com/typ1805/tansci

    GitHub: https://github.com/typ1805/tansci

    二、编码实现

    启动类添加 @EnableScheduling 注解

    @EnableScheduling
    @SpringBootApplication
    public class TansciApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(TansciApplication.class, args);
        }
    
    }
    

    定时任务类

    添加注解 @Component 注册到 spring 容器中。

    /**
     * @ClassName: ScheduledTask.java
     * @ClassPath: com.tansci.common.task.ScheduledTask.java
     * @Description: 定时任务
     * @Author: tanyp
     * @Date: 2022/2/25 9:30
     **/
    @Slf4j
    @Component
    public class ScheduledTask implements SchedulingConfigurer {
    
        private volatile ScheduledTaskRegistrar registrar;
    
        private final ConcurrentHashMap<String, ScheduledFuture<?>> scheduledFutures = new ConcurrentHashMap<>();
    
        private final ConcurrentHashMap<String, CronTask> cronTasks = new ConcurrentHashMap<>();
    
        @Autowired
        private TaskContextService taskContextService;
    
        @Override
        public void configureTasks(ScheduledTaskRegistrar registrar) {
            registrar.setScheduler(Executors.newScheduledThreadPool(Constants.DEFAULT_THREAD_POOL));
            this.registrar = registrar;
        }
    
        @PreDestroy
        public void destroy() {
            this.registrar.destroy();
        }
    
        /**
         * @MonthName: refreshTask
         * @Description: 初始化任务
         * 1、从数据库获取执行任务的集合【TaskConfig】
         * 2、通过调用 【refresh】 方法刷新任务列表
         * 3、每次数据库中的任务发生变化后重新执行【1、2】
         * @Author: tanyp
         * @Date: 2022/2/25 9:42
         * @Param: [tasks]
         * @return: void
         **/
        public void refreshTask(List<TaskConfig> tasks) {
            // 删除已经取消任务
            scheduledFutures.keySet().forEach(key -> {
                if (Objects.isNull(tasks) || tasks.size() == 0) {
                    scheduledFutures.get(key).cancel(false);
                    scheduledFutures.remove(key);
                    cronTasks.remove(key);
                    return;
                }
                tasks.forEach(task -> {
                    if (!Objects.equals(key, task.getTaskId())) {
                        scheduledFutures.get(key).cancel(false);
                        scheduledFutures.remove(key);
                        cronTasks.remove(key);
                        return;
                    }
                });
            });
    
            // 添加新任务、更改执行规则任务
            tasks.forEach(item -> {
                String expression = item.getExpression();
                // 任务表达式为空则跳过
                if (StringUtils.isEmpty(expression)) {
                    return;
                }
    
                // 任务已存在并且表达式未发生变化则跳过
                if (scheduledFutures.containsKey(item.getTaskId()) && cronTasks.get(item.getTaskId()).getExpression().equals(expression)) {
                    return;
                }
    
                // 任务执行时间发生了变化,则删除该任务
                if (scheduledFutures.containsKey(item.getTaskId())) {
                    scheduledFutures.get(item.getTaskId()).cancel(false);
                    scheduledFutures.remove(item.getTaskId());
                    cronTasks.remove(item.getTaskId());
                }
    
                CronTask task = new CronTask(new Runnable() {
                    @Override
                    public void run() {
                        // 执行业务逻辑
                        try {
                            log.info("====执行单个任务,任务ID【{}】执行规则【{}】=======", item.getTaskId(), item.getExpression());
                            taskContextService.execute(item.getCode());
                        } catch (Exception e) {
                            log.error("执行任务异常,异常信息:{}", e);
                        }
                    }
                }, expression);
                ScheduledFuture<?> future = registrar.getScheduler().schedule(task.getRunnable(), task.getTrigger());
                cronTasks.put(item.getTaskId(), task);
                scheduledFutures.put(item.getTaskId(), future);
            });
        }
    
    }
    

    任务自启动配置

    启动项目是读取任务配置表中的信息,初始化任务执行列表。

    /**
     * @ClassName: TaskApplicationRunner.java
     * @ClassPath: com.tansci.common.task.TaskApplicationRunner.java
     * @Description: 任务自启动配置
     * @Author: tanyp
     * @Date: 2022/2/25 9:43
     **/
    @Slf4j
    @Component
    public class TaskApplicationRunner implements ApplicationRunner {
    
        @Autowired
        private ScheduledTask scheduledTask;
    
        @Autowired
        private TaskConfigService taskConfigService;
    
        @Override
        public void run(ApplicationArguments args) {
            try {
                log.info("================项目启动初始化定时任务====开始===========");
                List<TaskConfig> tasks = taskConfigService.list(Wrappers.<TaskConfig>lambdaQuery().eq(TaskConfig::getStatus, 1));
                log.info("========初始化定时任务数为:{}=========", tasks.size());
                scheduledTask.refreshTask(tasks);
                log.info("================项目启动初始化定时任务====完成==========");
            } catch (Exception e) {
                log.error("================项目启动初始化定时任务====异常:{}", e);
            }
        }
    
    }
    

    任务配置相关类

    • TaskConfig: 任务配置实体
    • TaskConfigService: 接口
    • TaskConfigServiceImpl: 接口实现类
    • TaskConfigMapper: Mapper 接口
    /**
     * @ClassName: TaskConfig.java
     * @ClassPath: com.tansci.domain.system.TaskConfig.java
     * @Description: 任务配置
     * @Author: tanyp
     * @Date: 2022/2/25 9:35
     **/
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @TableName(value = "task_config")
    @ApiModel(value = "任务配置")
    public class TaskConfig {
    
        @ApiModelProperty(value = "主键id")
        @TableId(type = IdType.ASSIGN_UUID)
        private String id;
    
        @ApiModelProperty(value = "任务服务名称")
        private String code;
    
        @ApiModelProperty(value = "任务编码")
        private String taskId;
    
        @ApiModelProperty(value = "任务执行规则时间:cron表达式")
        private String expression;
    
        @ApiModelProperty(value = "任务名称")
        private String name;
    
        @ApiModelProperty(value = "状态:0、未启动,1、正常")
        private Integer status;
    
        @ApiModelProperty(value = "状态")
        @TableField(exist = false)
        private String statusName;
    
        @ApiModelProperty(value = "创建人")
        private String creater;
    
        @ApiModelProperty(value = "更新时间")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", locale = "zh", timezone = "GMT+8")
        private LocalDateTime updateTime;
    
        @ApiModelProperty(value = "创建时间")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", locale = "zh", timezone = "GMT+8")
        private LocalDateTime createTime;
    
        @ApiModelProperty(value = "描述")
        private String remarks;
    
    }
    

    动态调用任务配置

    通过 task_config 配置的 code 字段信息来调用业务实现 spring bean。

    /**
     * @ClassName: TaskContextServiceImpl.java
     * @ClassPath: com.tansci.service.impl.system.TaskContextServiceImpl.java
     * @Description: 动态调用任务配置信息
     * @Author: tanyp
     * @Date: 2022/2/25 10:12
     **/
    @Slf4j
    @Service
    public class TaskContextServiceImpl implements TaskContextService {
    
        /**
         * 任务注册器
         */
        @Autowired
        private Map<String, TaskRegisterService> componentServices;
    
        /**
         * @MonthName: execute
         * @Description: 解析器
         * @Author: tanyp
         * @Date: 2022/2/25 10:13
         * @Param: [taskServerName]
         * @return: void
         **/
        @Override
        public void execute(String taskServerName) {
            componentServices.get(taskServerName).register();
        }
    
    }
    

    任务注册器

    /**
     * @ClassName: TaskRegisterService.java
     * @ClassPath: com.tansci.service.system.TaskRegisterService.java
     * @Description: 任务注册器
     * @Author: tanyp
     * @Date: 2022/2/25 10:05
     **/
    public interface TaskRegisterService {
    
        void register();
    
    }
    

    创建业务实现时,只需实现 TaskRegisterService 接口即可。

    注意:
    @Service("taskTest1Service") 是唯一的,对应 task_config 表中的 code 字段;
    expression 的配置为 cron 表达式。

    创建两个任务测试类:

    @Slf4j
    @Service("taskTest1Service")
    public class TaskTest1ServiceImpl implements TaskRegisterService {
    
        @Override
        public void register() {
            log.info("===========自定义任务测试【TaskTest1ServiceImpl】====【1】=========");
        }
    }
    
    @Slf4j
    @Service("taskTest2Service")
    public class TaskTest2ServiceImpl implements TaskRegisterService {
    
        @Override
        public void register() {
            log.info("===========自定义任务测试【TaskTest2ServiceImpl】====【3】=========");
        }
    }
    

    三、测试

    在界面配置 taskTest1ServicetaskTest2Service 如下:

    添加配置

    任务配置列表

    启动项目,执行结果如下:

    2022-02-25 12:59:00,007  [pool-2-thread-1] INFO  com.tansci.common.task.ScheduledTask.run 107 - ====执行单个任务,任务ID【T1000214524DFS】执行规则【*/20 * * * * ?】=======
    2022-02-25 12:59:00,007  [pool-2-thread-1] INFO  com.tansci.service.impl.system.task.TaskTest1ServiceImpl.register 20 - ===========自定义任务测试【TaskTest1ServiceImpl】====【1】=========
    2022-02-25 12:59:20,015  [pool-2-thread-3] INFO  com.tansci.common.task.ScheduledTask.run 107 - ====执行单个任务,任务ID【T1000214524DFS】执行规则【*/20 * * * * ?】=======
    2022-02-25 12:59:20,015  [pool-2-thread-3] INFO  com.tansci.service.impl.system.task.TaskTest1ServiceImpl.register 20 - ===========自定义任务测试【TaskTest1ServiceImpl】====【1】=========
    2022-02-25 12:59:40,004  [pool-2-thread-3] INFO  com.tansci.common.task.ScheduledTask.run 107 - ====执行单个任务,任务ID【T1000214524DFS】执行规则【*/20 * * * * ?】=======
    2022-02-25 12:59:40,004  [pool-2-thread-3] INFO  com.tansci.service.impl.system.task.TaskTest1ServiceImpl.register 20 - ===========自定义任务测试【TaskTest1ServiceImpl】====【1】=========
    

    可以看到初始化的任务都在执行,并且是多线程在执行。

    四、cron 表达式

    corn 从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份

    字段 允许值 允许的特殊字符
    秒(Seconds) 0~59 的整数 , - * /
    分(Minutes) 0~59 的整数 , - * /
    小时(Hours) 0~23 的整数 , - * /
    日期(DayofMonth) 1~31 的整数 ,- * ? / L W C
    月份(Month) 1~12 的整数或者 JAN-DEC , - * /
    星期(DayofWeek) 1~7 的整数或者 SUN-SAT (1=SUN) , - * ? / L C #
    年(可选,留空)(Year) 1970~2099 , - * /
    • *:表示匹配该域的任意值。假如在 Minutes 域使用*, 即表示每分钟都会触发事件。
    • ?:只能用在 DayofMonth 和 DayofWeek 两个域。
    • -:表示范围。例如在 Minutes 域使用 5-20,表示从 5 分到 20 分钟每分钟触发一次
    • /:表示起始时间开始触发,然后每隔固定时间触发一次。
    • ,:表示列出枚举值。例如:在 Minutes 域使用 5,20,则意味着在 5 和 20 分每分钟触发一次。
    • L:表示最后,只能出现在 DayofWeek 和 DayofMonth 域。
    • W:表示有效工作日(周一到周五),只能出现在 DayofMonth 域,系统将在离指定日期的最近的有效工作日触发事件。
    • LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。
    • #:用于确定每个月第几个星期几,只能出现在 DayofMonth 域。例如在 4#2,表示某月的第二个星期三。

    常用表达式例子

    • 0 0 2 1 * ? * 表示在每月的 1 日的凌晨 2 点调整任务
    • 0 15 10 ? * MON-FRI 表示周一到周五每天上午 10:15 执行作业
    • 0 15 10 ? 6L 2002-2006 表示 2002-2006 年的每个月的最后一个星期五上午 10:15 执行作
    • 0 0 10,14,16 * * ? 每天上午 10 点,下午 2 点,4 点
    • 0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
    • 0 0 12 ? * WED 表示每个星期三中午 12 点
    • 0 0 12 * * ? 每天中午 12 点触发
    • 0 15 10 ? * * 每天上午 10:15 触发
    • 0 15 10 * * ? 每天上午 10:15 触发
    • 0 15 10 * * ? * 每天上午 10:15 触发
    • 0 15 10 * * ? 2005 2005 年的每天上午 10:15 触发
    • 0 * 14 * * ? 在每天下午 2 点到下午 2:59 期间的每 1 分钟触发
    • 0 0/5 14 * * ? 在每天下午 2 点到下午 2:55 期间的每 5 分钟触发
    • 0 0/5 14,18 * * ? 在每天下午 2 点到 2:55 期间和下午 6 点到 6:55 期间的每 5 分钟触发
    • 0 0-5 14 * * ? 在每天下午 2 点到下午 2:05 期间的每 1 分钟触发
    • 0 10,44 14 ? 3 WED 每年三月的星期三的下午 2:10 和 2:44 触发
    • 0 15 10 ? * MON-FRI 周一至周五的上午 10:15 触发
    • 0 15 10 15 * ? 每月 15 日上午 10:15 触发
    • 0 15 10 L * ? 每月最后一日的上午 10:15 触发
    • 0 15 10 ? * 6L 每月的最后一个星期五上午 10:15 触发
    • 0 15 10 ? * 6L 2002-2005 2002 年至 2005 年的每月的最后一个星期五上午 10:15 触发
    • 0 15 10 ? * 6#3 每月的第三个星期五上午 10:1 触发

    五、动态使用方式

    1、启动方式有两种

    • 启动项目后,手动调用 ScheduledTask.refreshTask(List<MyTask> tasks),并初始化任务列表;
    • 使用我测试中的方式,配置项目启动完成后自动调用初始任务的方法,并初始化任务列表。

    2、数据初始化

    只需要给 List<MyTask> 集合赋值并调用 refreshTask() 方法即可:

    • 根据业务需求修改 TaskConfig 实体类;
    • 这里的初始化数据可以从数据库读取数据赋值给集合;

    例如:从 mysql 读取任务配置表的数据,调用 refreshTask() 方法。

    3、如何动态

    • 修改:修改某一项正在执行的任务规则;
    • 添加:添加一项新的任务;
    • 删除:停止某一项正在执行的任务。

    例如:我们有一张任务配置表,此时进行分别新增一条或多条数据、删除一条或多条数据、改一条数据,只需要完成以上任何一项操作后,重新调用一下 refreshTask() 方法即可。

    怎么重新调用 refreshTask() 方法:可以另外启一个任务实时监控任务表的数据变化。

  • 相关阅读:
    Vscode 小白使用介绍
    Vue之父组件向子组件传递方法
    Vue之父组件向子组件传值
    Vue之组件切换
    Vue中组件的data和methods
    Vue之创建组件
    Vue生命周期代码示例
    Vue生命周期示例图
    inline、block和inline-block的区别
    系统测试测试过程
  • 原文地址:https://www.cnblogs.com/typ1805/p/15936366.html
Copyright © 2020-2023  润新知