项目开发过程中,或多或少会涉及到需要定时任务来执行处理的功能模块;而往往定时任务处理是很重要的一层;对定时任务的管控与监控就显得很重要。本文就如何对定时任务动态管理,与简单监控做以说明。

        工程github仓库地址:https://github.com/nirvana-x/dispatch_task

配置类:定时任务的加载,启动时,初始化好定时任务,本文是从数据库读取,初始化后,进行装配即可。然后在具体执行的时候加锁控制。

/**
 * @author
 * @Description 定时任务配置类
 * @Date 2019/5/14 18:47
 **/
@Component
@EnableScheduling
public class TaskConfig implements SchedulingConfigurer {

    
    private static final Logger logger = LoggerFactory.getLogger(TaskConfig.class);

    private static final int TIMEOUT = 5 * 1000;
    /**
     * 任务执行的间隔时间60秒 小于60秒时,不让其执行
     */
    private static final int INTERVAL_TIME = 30;
    private static final String LOCK_PREFIX = "lock_";


    private Map<String, ScheduledFuture<?>> futures = new HashMap<>();

    @Autowired
    private TaskBeanDao taskBeanDao;

    @Autowired
    private RedisLockHelper redisLockHelper;


    @Autowired
    @Qualifier("threadPoolTaskScheduler")
    private ThreadPoolTaskScheduler threadPoolTaskScheduler;

    /**
     * 模拟 schedule 配置,实际执⾏行行是从 t_task_config 表读取
     */
    private List<TaskBean> taskConfigs = new ArrayList<>();

    /**
     * 定时获取数据库最新的定时任务
     */
    @PostConstruct
    public void init() {
        taskConfigs = taskBeanDao.findByEnable(TaskBeanEnableEnum.ENABLE_STATUS.getType());
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        for (TaskBean taskBean : taskConfigs) {
            ScheduledFuture future = null;
            if (taskBean.getCron().contains("fixedDelay")) {
                long fixedDelay = Long.valueOf(taskBean.getCron().split("fixedDelay=")[1]);
                future = threadPoolTaskScheduler.scheduleWithFixedDelay(new TaskThread(taskBean), fixedDelay);
            } else {
                future = threadPoolTaskScheduler.schedule(new TaskThread(taskBean), new
                        CronTrigger(taskBean.getCron()));
            }
            futures.put(taskBean.getTaskCode(), future);
        }
    }

    public class TaskThread extends Thread {


        private TaskBean taskBean;

        public TaskThread(TaskBean taskBean) {
            this.taskBean = taskBean;
        }

        public TaskBean getTaskBean() {
            return taskBean;
        }

        public void setTaskBean(TaskBean taskBean) {
            this.taskBean = taskBean;
        }

        @Override
        public void run() {
            long time = System.currentTimeMillis() + TIMEOUT;
            String lock = LOCK_PREFIX + taskBean.getTaskCode();
            if (!redisLockHelper.lock(lock, String.valueOf(time))) {
                logger.info("【定时任务未获取到锁【{}】】", lock);
            } else {
                try {
                    //获取锁成功
                    logger.info("【定时任务获取锁{}成功】,任务编码:{},任务名称:{}", lock, taskBean.getTaskCode(), taskBean.getTaskName());
                    //获取最新的task任务执行信息
                    taskBean = taskBeanDao.findById(taskBean.getId()).orElse(null);
                    //TODO 验证执行时间是否有效 避免锁在毫秒级就被释放
                    long executeTime = (time - TIMEOUT - taskBean.getLastExecuteTime().getTime()) / 1000;
                    if (executeTime < INTERVAL_TIME) {
                        logger.info("【该任务已被其他实例执行】,任务编码:{},任务名称:{}", lock, taskBean.getTaskCode(),
                                taskBean.getTaskName());
                        return;
                    }
                    //调用业务模块进行处理
                    logger.info("【执行定时任务【编码:{}】开始】 任务名称:{}", taskBean.getTaskCode(), taskBean.getTaskName());
                    //2.2 合法性校验.
                    if (StringUtils.isEmpty(taskBean.getCron())) {
                        logger.info("【定时任务【编码:{}】执行计划为空】,任务名称:{}", taskBean.getTaskCode(), taskBean.getTaskName());
                    } else {
                        taskBean.setLastExecuteTime(new Date());
                        taskBeanDao.saveAndFlush(taskBean);
                        String postResult = HttpUtils.postHttp(taskBean.getUrl(), null, 30000);
                        logger.info("任务【编码:{}】调用业务层返回:{}", taskBean.getTaskCode(), postResult);
                    }
                    logger.info("【执行定时任务【编码:{}】结束】 任务名称:{}", taskBean.getTaskCode(), taskBean.getTaskName());
                } catch (Exception e) {
                    logger.error("【调用业务处理层报错】,任务编码:{},任务名称:{}", taskBean.getTaskCode(),
                            taskBean.getTaskName());
                    logger.error(e.getMessage(), e);
                } finally {
                    redisLockHelper.unlock(lock, String.valueOf(time));
                    logger.info("【定时任务释放锁【{}】成功】,任务编码:{},任务名称:{}", lock, taskBean.getTaskCode(),
                            taskBean.getTaskName());
                }
            }

        }

    }

    public Map<String, ScheduledFuture<?>> getFutures() {
        return futures;
    }

}

    任务锁处理类:获取锁与释放锁的处理工具类。

/**
 * @author
 * @Description ${TODO}
 * @Date 2019/5/23 9:59
 **/
@Component
public class RedisLockHelper {

    private static final Logger logger = LoggerFactory.getLogger(RedisLockHelper.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 加锁
     * @param targetId   targetId - 唯一标志
     * @param timeStamp  当前时间+超时时间 也就是时间戳
     * @return
     */
    public boolean lock(String targetId,String timeStamp){
        if(stringRedisTemplate.opsForValue().setIfAbsent(targetId,timeStamp)){
            // 对应setnx命令,可以成功设置,也就是key不存在
            return true;
        }
        // 判断锁超时 - 防止原来的操作异常,没有运行解锁操作  防止死锁
        String currentLock = stringRedisTemplate.opsForValue().get(targetId);
        // 如果锁过期 currentLock不为空且小于当前时间
        if(!Strings.isNullOrEmpty(currentLock) && Long.parseLong(currentLock) < System.currentTimeMillis()){
            // 获取上一个锁的时间value 对应getset,如果lock存在
            String preLock =stringRedisTemplate.opsForValue().getAndSet(targetId,timeStamp);

            // 假设两个线程同时进来这里,因为key被占用了,而且锁过期了。获取的值currentLock=A(get取的旧的值肯定是一样的),两个线程的timeStamp都是B,key都是K.锁时间已经过期了。
            // 而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的timeStamp已经变成了B。只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
            if(!Strings.isNullOrEmpty(preLock) && preLock.equals(currentLock) ){
                // preLock不为空且preLock等于currentLock,也就是校验是不是上个对应的时间戳,也是防止并发
                return true;
            }
        }
        return false;
    }


    /**
     * 解锁
     * @param target
     * @param timeStamp
     */
    public void unlock(String target,String timeStamp){
        try {
            String currentValue = stringRedisTemplate.opsForValue().get(target);
            if(!Strings.isNullOrEmpty(currentValue) && currentValue.equals(timeStamp) ){
                // 删除锁状态
                stringRedisTemplate.opsForValue().getOperations().delete(target);
            }
        } catch (Exception e) {
            logger.error("解锁异常{}",e);
        }
    }
}

Redis配置类:主要针对的是多实例间,任务同步问题,即某一实例任务执行计划有变动,则通知其他实例,也进行相应的变动。这里采用的是Redis的消息发布与订阅模式。这里重要的是订阅主题。一定要设置监听的topic。

@Configuration
@EnableCaching
public class RedisConfig {

    @Autowired
    private TaskBeanDao taskBeanDao;


    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
                new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(jackson2JsonRedisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashKeySerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                            MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        List<TaskBean> taskBeanList = taskBeanDao.findByEnable(1);
        taskBeanList.forEach(taskBean -> {
            //订阅主题
            container.addMessageListener(listenerAdapter, new PatternTopic(taskBean.getTaskCode()));
        });
        return container;
    }

    @Bean
    MessageListenerAdapter listenerAdapter(MessageReceive receiver) {
        //这个地方 是给messageListenerAdapter 传入一个消息接受的处理器,利用反射的方法调用“receiveMessage”
        //也有好几个重载方法,这边默认调用处理器的方法 叫handleMessage 可以自己到源码里面看
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }
}

消息订阅处理类

/**
 * @author
 * @Description redis 消息处理器
 * @Date 2019/6/4 14:25
 **/
@Component
public class MessageReceive {

    private static final Logger logger = LoggerFactory.getLogger(MessageReceive.class);

    @Autowired
    private TaskConfig taskConfig;

    @Autowired
    private TaskBeanDao taskBeanDao;

    /**
     * 接收消息的方法
     */
    public void receiveMessage(String message) {
        logger.info("【Redis 消息订阅】,{}",message);
        JSONArray object = JSON.parseArray(message);
        JSONObject jsonObject = object.getJSONObject(1);
        TaskBean taskBean = taskBeanDao.findById(jsonObject.getLong("id")).orElse(null);
        logger.info("【匹配定时任务为】,{}",JSONObject.toJSONString(taskBean));
        if (taskBean.getEnable().equals(TaskBeanEnableEnum.DISABLE_STATUS.getType())){
            //说明定时任务是未启用(暂停定时任务)
            taskConfig.cancelTask(taskBean);
        }else{
            //说明定时任务是新增或者修改
            taskConfig.saveOrUpdateTaskConfigs(taskBean);
        }
    }
}

最后,在任务变动的业务处理模块只需要添加如下代码即可。

//Redis发布消息通知,通知其他实例变动
        logger.info("【Redis发布消息通知,刷新定时任务,通知其他实例变动】,任务名称:{}", taskBean.getTaskName());
        redisTemplate.convertAndSend(taskBean.getTaskCode(), taskBean);