基于nacos 手写分布式定时任务

609 阅读4分钟

1,为什么要基于nacos 写分布式的动态定时任务

在日常的开发过程中, 定时任务是很常见的。基本上都是使用注解形式的开发。现在的服务基本上都是微服务分布式的结构。基本上每个服务都会多个实例, 这个时候就会有分布式定时任务的需求了。 但是又不想使用第三方的开源中间件,这个时候基于redis + nacos 写一个分布式的定时任务组件。

2,分布式动态定时任务的架构和思路

2,1 思路,还是使用自定义注解形式+自定义starter 代码替换灵活,修改少。

2.1.1 首先初始化的时候,从配置中心拉取配置文件形成map 和 本地注解的key 对应.多个实例在根据redis 是否存在。来判断只启动一个定时任务。 服务下线的时候在判断是不是最后一个实例决定是否销毁。其中还使用了redis的分布式锁。

2.1.2 在使用nacos 的监听器, 监听到nacos 配置文件的改变。 根据改变的key 来本地修改,要判断是否在当前服务起来的。redis 是否存在这个key ,才能动态的修改当前服务的这个schedule.的定时任务。

2.2 具体的代码架构

annotation 的包里面都是自定义的注解, 里面包含两个注解,一个是使用在启动类上,表示开启动态定时任务, 另外一个是使用在方法上面。都是和springboot自带的定时任务注解很相似。

common 包里面是常量,主要是保存一些定时任务的信息和对应的线程信息

config 是配置类,也是核心类,里面使用了springboot 的 bean 的后置处理器。会初始化启动的时候从nacos 读取配置文件。封装成scheduling的定时任务对象。 本地缓存nacos 配置文件的key 和定时任务的关系。并且还会把一些定时任务信息组装放到常量池缓存里面。

task 包 是基础类,是真正的定时任务执行的类,里面封装了定时任务的元数据和自定义线程池。

3,主要代码

package com.ducheng.distributed.dynamic.schedule.config;


import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import com.alibaba.fastjson.JSONObject;
import com.ducheng.distributed.dynamic.schedule.utils.SpringUtils;
import com.ducheng.distributed.dynamic.schedule.utils.StrUtil;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.ducheng.distributed.dynamic.schedule.common.ConstantsPool;
import com.ducheng.distributed.dynamic.schedule.annotation.DynamicScheduled;
import com.ducheng.distributed.dynamic.schedule.task.CustomCronTaskRegister;
import com.ducheng.distributed.dynamic.schedule.task.DcSchedulingRunnable;

import static com.ducheng.distributed.dynamic.schedule.common.ConstantsPool.SERVICE_NUMBER;

public class DynamicSchedulingAutoRegistryProcess implements BeanPostProcessor, CommandLineRunner {

    @Autowired
    private NacosConfigProperties nacosConfigProperties;

    @Value("${spring.cloud.nacos.config.distributed.dynamic.schedule-id}")
    private String dataId;

    @Autowired
    private RedissonClient redissonClient;


    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        Method[] methods = ReflectionUtils.getAllDeclaredMethods(bean.getClass());
        if (methods == null) return bean;
        for (Method method : methods) {
            DynamicScheduled dcsScheduled = AnnotationUtils.findAnnotation(method, DynamicScheduled.class);
            //初始化加载定时任务
            if (!ObjectUtils.isEmpty(dcsScheduled)) {
                String resolve = StrUtil.resolveKey(dcsScheduled.cron());
                DcSchedulingRunnable schedulingRunnable = new DcSchedulingRunnable(bean,beanName,method.getName());
                //把他放到缓存里面
                if (!ConstantsPool.PROPERTIES_TASK_IDS.containsKey(resolve)) {
                    List<String> list = new ArrayList<>();
                    list.add(schedulingRunnable.getTaskId());
                    ConstantsPool.PROPERTIES_TASK_IDS.put(resolve,list);
                } else {
                    List<String> list = ConstantsPool.PROPERTIES_TASK_IDS.get(resolve);
                    list.add(schedulingRunnable.getTaskId());
                }
                ConstantsPool.RUNNABLE_MAP.put(schedulingRunnable.getTaskId(),schedulingRunnable);
            }
        }
        return bean;
    }

    @Bean("dynamic-schedule-taskScheduler")
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        //线程池使用的就是当前线程的大小
        taskScheduler.setPoolSize(Runtime.getRuntime().availableProcessors());
        taskScheduler.setRemoveOnCancelPolicy(true);
        taskScheduler.setThreadNamePrefix("DynamicScheduleThreadPool-");
        return taskScheduler;
    }

    @Bean
    public CustomCronTaskRegister getCustomCronTaskRegister() {
        return new CustomCronTaskRegister();
    }

    @Bean
    @ConditionalOnBean(NacosConfigProperties.class)
    public DynamicSchedulingConfiguration getDynamicSchedulingConfiguration(@Autowired CustomCronTaskRegister customCronTaskRegister) {
        return new DynamicSchedulingConfiguration(customCronTaskRegister);
    }

    @Override
    public void run(String... args) throws Exception {
        // add service number
        RAtomicLong atomicLong = redissonClient.getAtomicLong(SERVICE_NUMBER);
        // add one
        atomicLong.addAndGet(1);

        ConfigService configService = NacosFactory.createConfigService(nacosConfigProperties.assembleConfigServiceProperties());
        // 程序首次启动, 并加载初始动态定时任务的配置
        String initConfigInfo = configService.getConfig(dataId, nacosConfigProperties.getGroup(), 5000);
        // 把配置文件解析成key value 的模式
        //换行符\n
        String lines[] = initConfigInfo.split("\r?\n");
        String toJSONString = JSONObject.toJSONString(lines);
        RLock lock = redissonClient.getLock(toJSONString);
        lock.lock();
        try {
            //在转成map
            for (String str: lines) {
                String[] split = str.split(": ");
                if (ConstantsPool.PROPERTIES_TASK_IDS.containsKey(split[0])) {
                    List<String> taskIds  = ConstantsPool.PROPERTIES_TASK_IDS.get(split[0]);
                    String cronExpression = split[1];
                    addTask(taskIds,cronExpression);
                }
            }
        }finally {
            lock.unlock();
        }
    }

    /**
     * 添加到定时任务
     * @param taskIds
     * @param cronExpression
     */
    public void addTask(List<String> taskIds,String cronExpression) {
        taskIds.stream().forEach(x-> {
            Boolean aBoolean = stringRedisTemplate.hasKey(x);
            if (!aBoolean) {
                SpringUtils.getBean(CustomCronTaskRegister.class).addCronTask(x,cronExpression);
                stringRedisTemplate.opsForValue().set(x,x);
            }
        });
    }
}
package com.ducheng.distributed.dynamic.schedule.config;

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

import com.alibaba.fastjson.JSONObject;
import com.ducheng.distributed.dynamic.schedule.utils.SpringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import com.ducheng.distributed.dynamic.schedule.common.ConstantsPool;
import com.ducheng.distributed.dynamic.schedule.task.CustomCronTaskRegister;

/**
 * 这里就是一个监听器,监听nacos 配置文件动态变化的key , 在动态的刷新定时任务
 */
public class DynamicSchedulingConfiguration implements EnvironmentAware,  ApplicationListener<EnvironmentChangeEvent> {

    private static  RedissonClient redissonClient =  SpringUtils.getBean(RedissonClient.class);

    private Environment environment;

    private CustomCronTaskRegister customCronTaskRegister;

    public DynamicSchedulingConfiguration(CustomCronTaskRegister customCronTaskRegister) {
        this.customCronTaskRegister = customCronTaskRegister;
    }

    @Override
    public void onApplicationEvent(EnvironmentChangeEvent event) {
        List<String> list = new ArrayList<String>(event.getKeys());
        //这里要加上redislock的分布式锁, 保证只有一个实例来执行。
        String toJSONString = JSONObject.toJSONString(list);
        RLock lock = redissonClient.getLock(toJSONString);
        lock.lock();
        try {
            for (String str : list) {
                if (ConstantsPool.PROPERTIES_TASK_IDS.containsKey(str)) {
                    List<String> taskIds  = ConstantsPool.PROPERTIES_TASK_IDS.get(str);
                    String cronExpression = environment.getProperty(str);
                    addTask(taskIds,cronExpression);
                }
            }
        }finally {
            lock.unlock();
        }
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }


    /**
     * 添加到定时任务
     * @param taskIds
     * @param cronExpression
     */
    public void addTask(List<String> taskIds,String cronExpression) {
        taskIds.stream().forEach(x-> {
            customCronTaskRegister.addCronTask(x,cronExpression);
        });
    }
}
package com.ducheng.distributed.dynamic.schedule.task;

import com.ducheng.distributed.dynamic.schedule.common.ConstantsPool;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.config.CronTask;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;

import static com.ducheng.distributed.dynamic.schedule.common.ConstantsPool.SERVICE_NUMBER;

/**
 *  定时任务注册器
 */
public class CustomCronTaskRegister implements DisposableBean {

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


    @Resource(name = "dynamic-schedule-taskScheduler")
    private TaskScheduler taskScheduler;

    @Autowired
    private RedissonClient redissonClient;


    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    public void addCronTask(String taskId, String cronExpression) {
        if (null != ConstantsPool.TASK_CONCURRENT_HASH_MAP.get(taskId)) {
            removeCronTask(taskId);
        }
        DcSchedulingRunnable dcSchedulingRunnable = ConstantsPool.RUNNABLE_MAP.get(taskId);
        CronTask cronTask = new CronTask(dcSchedulingRunnable, cronExpression);
        ConstantsPool.TASK_CONCURRENT_HASH_MAP.put(taskId, scheduleCronTask(cronTask));
        logger.info("添加定时任务成功,定时任务的cron表达式:{}, taskId:{}",cronExpression,taskId);
    }



    public void removeCronTask(String taskId) {
        ScheduledTask scheduledTask = ConstantsPool.TASK_CONCURRENT_HASH_MAP.remove(taskId);
        if (scheduledTask == null) return;
        scheduledTask.cancel();
        logger.info("取消定时任务成功, taskId :{}",taskId);
    }

    private ScheduledTask scheduleCronTask(CronTask cronTask) {
        ScheduledTask scheduledTask = new ScheduledTask();
        scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());
        return scheduledTask;
    }

    @Override
    public void destroy() {
        ConstantsPool.TASK_CONCURRENT_HASH_MAP.clear();
        ConstantsPool.RUNNABLE_MAP.clear();
        Collection<List<String>> values = ConstantsPool.PROPERTIES_TASK_IDS.values();
        RAtomicLong atomicLong = redissonClient.getAtomicLong(SERVICE_NUMBER);
        Long number = atomicLong.get();
        if (number.intValue() == 1 ) {

            values.stream().forEach(x->{
                x.stream().forEach(y-> {
                    stringRedisTemplate.delete(y);
                });
            });
        }
        ConstantsPool.PROPERTIES_TASK_IDS.clear();
        atomicLong.decrementAndGet();
    }
}

4,怎么使用

使用教程:
ducheng.github.io/distributed…