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,怎么使用