需求背景
首先为什么要自研分布式任务调度中间件呢?
- 定制化需求:因为可能有特定的业务需求,需要定制化的分布式任务调度框架来满足自身的业务场景和特定需求。
- 自研框架可以让我们更好地掌控自身的技术发展和业务发展,降低对外部框架的依赖和风险。
方案设计
因为需要实现一个分布式的任务调度系统,那么就需要把每一个服务通过中间件引入,成为任务调度中算力的一部分。同时将算力服务发布到注册中心,做到统一控制。
设计开发
注解开发
这个注解是配置到应用程序的 XxxApplication 上,为了启动执行开启中间件的内容。
Import 引入入口资源,在程序启动时会执行到自己定义的类中,以方便我们;初始化配置/服务、启动任务、挂在节点。
ComponentScan 告诉程序扫描位置,这里配置的位置就是中间件本身的位置。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ComponentScan("com.rzp.middleware.*")
public @interface EnableDcsScheduling {
}
用法
例如可以在测试工程的启动类上加入
@SpringBootApplication
@EnableDcsScheduling
public class ApiTestApplication {
public static void main(String[] args) {
SpringApplication.run(ApiTestApplication.class, args);
}
}
定时任务标记的注解开发 @DcsScheduled
用于家在bean中的方法中标注为注册到中间件中的定时任务。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DcsScheduled {
// 当前任务的描述
String desc() default "缺省";
// 当前任务的cron表达式
String cron() default "";
// 表示当前任务是否配置启动
boolean autoStartup() default true;
}
启动类开发 (只贴出一些主要的代码逻辑)
StarterAutoConfig 引入组件后自动装配这个类 类中包含了读取配置的yml文件信息的properties类
@Configuration("middleware-schedule-starterAutoConfig")
@EnableConfigurationProperties(StarterServiceProperties.class)
public class StarterAutoConfig {
@Autowired
private StarterServiceProperties properties;
public StarterServiceProperties getProperties() {
return properties;
}
public void setProperties(StarterServiceProperties properties) {
this.properties = properties;
}
}
StarterServiceProperties 类
@ConfigurationProperties("middleware.schedule")
public class StarterServiceProperties {
private String zkAddress; //zookeeper服务地址;x.x.x.x:2181
private String schedulerServerId; //任务服务ID; 工程名称En
private String schedulerServerName; //任务服务名称;工程名称
以及对应的get set方法
}
DcsSchedulingConfiguration 最核心的配置类实现了上一步骤中注解的扫描;zk的client创建;对应节点的连接,定时任务的收集,启动定时任务,心跳检测的方法
DcsSchedulingConfiguration实现了BeanPostProcessor,重写了postProcessAfterInitialization方法。相当于一个后处理器会扫描全部加了@DcsScheduled注解的方法保存起来
后置处理器收集注解方法
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
if (this.nonAnnotatedClasses.contains(targetClass)) return bean;
// 拿到当前bean实例的全部方法
Method[] methods = ReflectionUtils.getAllDeclaredMethods(bean.getClass());
for (Method method : methods) {
// 查看当前bean的方法中有加了DcsScheduled注解的方法吗?
DcsScheduled dcsScheduled = AnnotationUtils.findAnnotation(method, DcsScheduled.class);
if(null == dcsScheduled || 0 == method.getDeclaredAnnotations().length) continue;
// 收集当前bean的全部加了@DcsScheduled注解的方法
List<ExecOrder> execOrderList = Constants.execOrderMap.computeIfAbsent(beanName, k -> new ArrayList<>());
ExecOrder execOrder = new ExecOrder();
execOrder.setBean(bean);
execOrder.setBeanName(beanName);
execOrder.setMethodName(method.getName());
execOrder.setDesc(dcsScheduled.desc());
execOrder.setCron(dcsScheduled.cron());
execOrder.setAutoStartup(dcsScheduled.autoStartup());
execOrderList.add(execOrder);
// 防止方法被重复添加
this.nonAnnotatedClasses.add(targetClass);
}
return bean;
实现了ApplicationListener监听spring容器refresh动作
方法里启用各种配置信息
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
// 1. 初始化配置
init_config(applicationContext);
// 2. 初始化服务
init_server(applicationContext);
// 3. 启动任务
init_task(applicationContext);
// 4. 挂载节点
init_node();
// 5. 心跳检测
HeartbeatService.getInstance().startFlushScheduleStatus();
初始化配置
其中init_config通过将读取到的properties信息存入一个全局的信息管理类Config,包括zk地址,服务名,服务id等。
init_server创建zk的client,同时创建使用了当前中间件服务的对应节点信息,同时为整个树添加上监听,监听全部子节点数据的变化。
treeCache.getListenable().addListener(((curatorFramework, event) -> {
// 获得变化的对象
byte[] eventData = event.getData().getData();
// 判断变化对象的类型如果是NODE_UPDATED
// 根据对象修改之后任务的状态去更新
switch (status) {
case 0:
// 调用registrar去移除当前任务 (此前任务已经全部收集到Constants.scheduledTasks中了)
cronTaskRegistrar.removeCronTask(instruct.getBeanName() + "_" + instruct.getMethodName());
setData(client, path_root_server_ip_clazz_method_status, "0");
logger.info("middleware schedule task stop {} {}", instruct.getBeanName(), instruct.getMethodName());
break;
case 1:
cronTaskRegistrar.addCronTask(new SchedulingRunnable(scheduleBean, instruct.getBeanName(), instruct.getMethodName()), instruct.getCron());
setData(client, path_root_server_ip_clazz_method_status, "1");
logger.info("middleware schedule task start {} {}", instruct.getBeanName(), instruct.getMethodName());
break;
case 2:
cronTaskRegistrar.removeCronTask(instruct.getBeanName() + "_" + instruct.getMethodName());
cronTaskRegistrar.addCronTask(new SchedulingRunnable(scheduleBean, instruct.getBeanName(), instruct.getMethodName()), instruct.getCron());
setData(client, path_root_server_ip_clazz_method_status, "2");
logger.info("middleware schedule task refresh {} {}", instruct.getBeanName(), instruct.getMethodName());
break;
}
}
初始化任务
init_task将经过后处理器收集到的全部加了@DcsScheduled 注解的方法收集提交到TaskScheduler。
单独配置一个任务调度器用来在init_taks接收任务
@Configuration("middlware-schedule-schedulingConfig")
public class SchedulingConfig {
@Bean("middlware-schedule-taskScheduler")
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(Constants.Global.schedulePoolSize);
taskScheduler.setRemoveOnCancelPolicy(true);
taskScheduler.setThreadNamePrefix("MiddlewareScheduleThreadPool-");
return taskScheduler;
}
}
任务注册器负责 添加&取消 任务
@Component("middlware-schedule-cronTaskRegister")
public class CronTaskRegister implements DisposableBean {
// taskScheduler就相当于一个线程池去执行任务
@Resource(name = "middlware-schedule-taskScheduler")
private TaskScheduler taskScheduler;
// 添加一个定时任务. 其中SchedulingRunnable就是implementsRunnable接口的一个任务,run执行时反射调用bean中注解标注的方法
public void addCronTask(SchedulingRunnable task, String cron) {
if (null != Constants.scheduledTasks.get(task.taskId())) {
removeCronTask(task.taskId());
}
// 创建一个cronTask
CronTask cronTask = new CronTask(task,cron);
// bean-name + method name 将任务信息收集起来
Constants.scheduledTasks.put(task.taskId(), scheduleCronTask(cronTask));
}
// 提交任务
private ScheduledTask scheduleCronTask(CronTask cronTask) {
ScheduledTask scheduledTask = new ScheduledTask();
scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());
return scheduledTask;
}
// bean销毁时取消任务
@Override
public void destroy() throws Exception {
for (ScheduledTask task : Constants.scheduledTasks.values()) {
task.cancel();
}
Constants.scheduledTasks.clear();
}
}
初始化节点
init_node也就是将所有的bean和对应的方法创建对应的zk节点。同时为节点赋值
private void init_node() throws Exception {
// 拿到全部任务的bean的名字
Set<String> beanNames = Constants.execOrderMap.keySet();
for (String beanName : beanNames) {
// 拿到这个bean中标注了分布式任务注解的全部ExecOrder对象
List<ExecOrder> execOrderList = Constants.execOrderMap.get(beanName);
for (ExecOrder execOrder : execOrderList) {
// /cn/middleware/schedule/server/001/ip/127.0.0.1/clazz/test
String path_root_server_ip_clazz = StrUtil.joinStr(path_root_server_ip, LINE, "clazz", LINE, execOrder.getBeanName());
// /cn/middleware/schedule/server/001/ip/127.0.0.1/clazz/test/method/schedule01
String path_root_server_ip_clazz_method = StrUtil.joinStr(path_root_server_ip_clazz, LINE, "method", LINE, execOrder.getMethodName());
// /cn/middleware/schedule/server/001/ip/127.0.0.1/clazz/test/method/schedule01/status
String path_root_server_ip_clazz_method_status = StrUtil.joinStr(path_root_server_ip_clazz, LINE, "method", LINE, execOrder.getMethodName(), "/status");
// 创建节点
ZkCuratorServer.createNodeSimple(client, path_root_server_ip_clazz);
ZkCuratorServer.createNodeSimple(client, path_root_server_ip_clazz_method);
ZkCuratorServer.createNodeSimple(client, path_root_server_ip_clazz_method_status);
// 添加临时节点数据
ZkCuratorServer.appendPersistentData(client, path_root_server_ip_clazz_method + "/value", JSON.toJSONString(execOrder));
// 添加节点数据 永久
ZkCuratorServer.setData(client, path_root_server_ip_clazz_method_status, execOrder.getAutoStartup() ? "1" : "0");
}
}
}
心跳检测
当前端页面将线程池任务的状态进行更改时及时异步同步到zk中。
public void startFlushScheduleStatus() {
// 创建一个定时任务线程池
ses = Executors.newScheduledThreadPool(1);
// 300秒之后执行,每60秒执行一次
ses.scheduleAtFixedRate(()->{
try {
logger.info("middleware schedule heart beat On-Site Inspection task");
// 收集了全部的定时任务 key是beanname + methodName ----> 定时任务
Map<String, ScheduledTask> scheduledTasks = Constants.scheduledTasks;
// 全部的bean同时bean里面加了定时任务注解的列表
Map<String, List<ExecOrder>> execOrderMap = Constants.execOrderMap;
Set<String> beanNameSet = execOrderMap.keySet();
// 心跳检测 检测全部的方法.
for (String beanName : beanNameSet) {
List<ExecOrder> execOrderList = execOrderMap.get(beanName);
for (ExecOrder execOrder : execOrderList) {
// map中全部的任务是通过beanname + _ + methodName拼接成的。
String taskId = execOrder.getBeanName() + "_" + execOrder.getMethodName();
ScheduledTask scheduledTask = scheduledTasks.get(taskId);
if (null == scheduledTask) continue;
// 查看当前任务的状态
boolean cancelled = scheduledTask.isCancelled();
// 路径拼装
String path_root_server_ip_clazz = StrUtil.joinStr(path_root_server_ip, LINE, "clazz", LINE, execOrder.getBeanName());
String path_root_server_ip_clazz_method = StrUtil.joinStr(path_root_server_ip_clazz, LINE, "method", LINE, execOrder.getMethodName(), LINE, "value");
ExecOrder oldExecOrder;
// 获取到当前方法的值
byte[] bytes = client.getData().forPath(path_root_server_ip_clazz_method);
if (null != bytes) {
String oldJson = new String(bytes, CHARSET_NAME);
oldExecOrder = JSON.parseObject(oldJson, ExecOrder.class);
}else{
oldExecOrder = new ExecOrder();
oldExecOrder.setBeanName(execOrder.getBeanName());
oldExecOrder.setMethodName(execOrder.getMethodName());
oldExecOrder.setDesc(execOrder.getDesc());
oldExecOrder.setCron(execOrder.getCron());
oldExecOrder.setAutoStartup(execOrder.getAutoStartup());
}
oldExecOrder.setAutoStartup(!cancelled);
if (null == Constants.Global.client.checkExists().forPath(path_root_server_ip_clazz_method)){
continue;
}
String newJson = JSON.toJSONString(oldExecOrder);
Constants.Global.client.setData().forPath(path_root_server_ip_clazz_method, newJson.getBytes(CHARSET_NAME));
String path_root_ip_server_clazz_method_status = StrUtil.joinStr(path_root_server_ip_clazz, LINE, "method", LINE, execOrder.getMethodName(), "/status");
if (null == Constants.Global.client.checkExists().forPath(path_root_ip_server_clazz_method_status))
continue;
Constants.Global.client.setData().forPath(path_root_ip_server_clazz_method_status, (execOrder.getAutoStartup() ? "1" : "0").getBytes(CHARSET_NAME));
}
}
}catch (Exception ignore){
}
},300,60, TimeUnit.SECONDS);
}
controller模块
提供了具体的前端页面和接口来通过页面方式修改任务信息。
例如如果模块中引入了当前中间件可以通过访问本地端口(7397)来打开管理页面。
在自己项目中配置的定时任务