基於SchedulingConfigurer的任務調度

一. SchedulingConfigurer解析
    Spring 中,創建定時任務除了使用@Scheduled 註解外,還可以使用 SchedulingConfigurer。既然兩者都可以實現定時任務,那有什麼不同呢?
    @Schedule註解的一個缺點就是其定時時間不能動態更改,它適用於具有固定任務週期的任務,若要修改任務執行週期,只能走“停服務→修改任務執行週期→重啓服務”這條路。而基於 SchedulingConfigurer 接口方式可以做到。SchedulingConfigurer 接口可以實現在@Configuration 類上,同時不要忘了,還需要@EnableScheduling 註解的支持。

package org.springframework.scheduling.annotation;

import org.springframework.scheduling.config.ScheduledTaskRegistrar;

@FunctionalInterface
public interface SchedulingConfigurer {
    void configureTasks(ScheduledTaskRegistrar var1);
}

ScheduledTaskRegistrar類包括以下幾個重要方法:
在這裏插入圖片描述
從方法的命名上可以猜到,方法包含定時任務,延時任務,基於 Cron 表達式的任務,以及 Trigger 觸發的任務。

二. 代碼示例

create table if not exists sys_schedule_lock(
    task_id varchar(32) primary key not null comment '任務類型id',
    task_desc varchar(32) default null comment '任務類型說明',
    exec_ip varchar(32) default null comment '獲取鎖的機器ip',
    acquire_time datetime DEFAULT NULL comment '獲取鎖的時間',
    status int(11) comment '當前任務id鎖狀態: 0-空閒 1-佔用'
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='定時任務執行數據庫鎖';

INSERT INTO zzxypm.sys_schedule_lock (task_id, task_desc, exec_ip, acquire_time, status) VALUES ('TASK_TODO', '定時推送待辦任務', '127.0.0.1', null, 0);
@NoArgsConstructor
@Data
@TableName("sys_schedule_lock")
public class ScheduleLock {
    @TableId(value = "task_id")
    private String taskId;
    private String taskDesc;
    private Date acquireTime;
    private String execIp;
    private int status;
}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zzxypm.entity.ScheduleLock;

import java.util.Map;

public interface ScheduleLockMapper extends BaseMapper<ScheduleLock> {
    int lock(Map<String,Object> map);
    int unlock(Map<String,Object> map);
    int batchResetStatus();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzxypm.mapper.ScheduleLockMapper">

   <update id="lock" parameterType="map">
       update sys_schedule_lock
       set exec_ip = #{execIp},
           acquire_time = now(),
           status = 1
       where task_id = #{taskId}
       and status = 0;
   </update>

    <update id="unlock" parameterType="map">
       update sys_schedule_lock
       set
           acquire_time = now(),
           status = 0
       where task_id = #{taskId}
       and status = 1
       and exec_ip = #{execIp};
   </update>

   <update id="batchResetStatus">
           update sys_schedule_lock
           set status = 0
           where status = 1
   </update>
</mapper>
package com.zzxypm.schedule;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public enum TaskIdEnum {
    TASK_TODO("TASK_TODO", "定時推送待辦任務");

    TaskIdEnum(String taskId, String desc) {
        this.taskId = taskId;
        this.desc = desc;
    }

    /**
     * 任務唯一id
     */
    private String taskId;

    /**
     * 任務類型描述
     */
    private String desc;


    public String getTaskId() {
        return taskId;
    }

    public String getDesc() {
        return desc;
    }

    public static TaskIdEnum acquire(final String taskId) {
        Optional<TaskIdEnum> serializeEnum =
                Arrays.stream(TaskIdEnum.values())
                        .filter(v -> Objects.equals(v.getTaskId(), taskId))
                        .findFirst();
        return serializeEnum.orElse(TaskIdEnum.TASK_TODO);
    }

    public static List<TaskIdEnum> getEnumList() {
        return Arrays.asList(TaskIdEnum.values());
    }
}
package com.zzxypm.schedule;

import com.zzxypm.common.util.DateUtil;
import com.zzxypm.common.util.IPV4Util;
import com.zzxypm.common.util.SpringContextHolder;
import com.zzxypm.mapper.ScheduleLockMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.Map;

/**
 * 抽象任務骨架定義
 */
@Slf4j
public abstract class AbstractTask implements Runnable{

    private TaskIdEnum taskIdEnum;
    private String cron;
    
    private ScheduleLockMapper lockMapper = SpringContextHolder.getBean(ScheduleLockMapper.class);

    public AbstractTask(TaskIdEnum taskIdEnum,String cron) {
        this.taskIdEnum = taskIdEnum;
        this.cron = cron;
    }

    @Transactional
    @Override
    public void run() {
        String taskId = taskIdEnum.getTaskId();
        String ip = IPV4Util.getLocalIpv4Address();
        Map<String,Object> map = new HashMap<>();
        map.put("taskId",taskId);
        map.put("execIp",ip);
        int lock = lockMapper.lock(map);
        log.info("lock: taskId=[{}],ip=[{}],lock=[{}]", taskId, ip, lock);
        if (lock == 1) {
            log.info("taskId:[{}],任務執行開始時間:[{}]", taskId, DateUtil.now());
            doWork();
            log.info("taskId:[{}],任務執行結束時間:[{}]", taskId, DateUtil.now());

            int unlock = lockMapper.unlock(map);
            log.info("unlock: taskId=[{}],ip=[{}],unlock=[{}]", taskId, ip, unlock);
        }
    }

    protected abstract void doWork();

    public TaskIdEnum getTaskIdEnum() {
        return taskIdEnum;
    }

    public String getCron() {
        return cron;
    }
}
package com.zzxypm.schedule.task;

import com.zzxypm.schedule.AbstractTask;
import com.zzxypm.schedule.TaskIdEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * 待辦推送任務
 */
@Slf4j
public class TodoPushTask extends AbstractTask {

    private static TaskIdEnum taskIdEnum = TaskIdEnum.TASK_TODO;

    // 每隔5秒執行一次
    private static final String CRON = "*/5 * * * * ?";

    public TodoPushTask() {
        super(taskIdEnum,CRON);
    }

    @Override
    protected void doWork() {
        //定時從數據庫中掃表,封裝爲 定時任務對象,丟到 工廠裏 讓 執行器 執行
        log.info("TodoPushTask,推送待辦任務");
    }
}
 package com.zzxypm.config;

import com.zzxypm.common.support.cls.DefaultClassScanner;
import com.zzxypm.schedule.AbstractTask;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.config.TriggerTask;
import org.springframework.scheduling.support.CronTrigger;

import java.util.ArrayList;
import java.util.List;

/**
 * 動態定時任務配置
 * (配置數據庫動態執行)
 **/
@Slf4j
@Configuration
public class DynamicScheduleConfig implements SchedulingConfigurer {

    @Autowired
    private DefaultClassScanner classScanner;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        try {
            List<Class<?>> taskClassList = classScanner.getClassListBySuper("com.zzxypm.schedule", AbstractTask.class);
            if (CollectionUtils.isNotEmpty(taskClassList)) {
                List<TriggerTask> taskList = new ArrayList<>(taskClassList.size());
                TriggerTask task = null;
                for (Class<?> cls : taskClassList) {
                    AbstractTask abstractTask = (AbstractTask) cls.newInstance();
                    String cron = abstractTask.getCron();
                    task = new TriggerTask((AbstractTask) cls.newInstance(),
                            triggerContext -> new CronTrigger(cron).nextExecutionTime(triggerContext)
                    );
                    taskList.add(task);
                }
                taskRegistrar.setTriggerTasksList(taskList);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 默認的,SchedulingConfigurer 使用的也是單線程的方式,如果需要配置多線程,則需要指定 PoolSize
     * @return
     */
    @Bean("taskScheduler")
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(20);
        taskScheduler.setThreadNamePrefix("Scheduled-");
        taskScheduler.setRejectedExecutionHandler((r, e) -> {
            if (!e.isShutdown()) {
                r.run();
            }
            // 記錄執行失敗的任務到數據庫表中
            // 發送告警郵件給相關負責人
        });
        taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        taskScheduler.setAwaitTerminationSeconds(60);
        taskScheduler.initialize();
        return taskScheduler;
    }
}

三. 動態配置定時規則
在不重啓服務的情況下,如何動態的修改定時任務的cron參數?
網上有兩種方法,都有缺點。

  1. https://blog.csdn.net/xht555/article/details/53121962
    此方法,是在觸發運行的時候,刷新定時規則,這種方法的缺點是,刷新規則的時間必須是在某次觸發運行的時候。

  2. https://blog.csdn.net/jianggujin/article/details/77937316
    此方法基於 SchedulingConfigurer 的源碼,捕獲 ScheduledTaskRegistrar 類的實例,通過該類中的 TaskScheduler 實例操作定時任務的增刪,而非採用 ScheduledTaskRegistrar.addTriggerTask 方法維護定時任務。所以需要自行寫代碼維護定時任務列表,控制任務的刪減,代碼的實現比較繁瑣。

如果想要實現可以動態修改的定時策略,建議使用開源組件 Quartz。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章