1. Uncode-Schedule功能概述
Uncode-Schedule是基于zookeeper的分布式任务调度组件,非常小巧,使用简单。
1.1. 它能够确保所有任务在集群中不重复,不遗漏的执行。
1.2. 单节点故障时,任务能够自动转移到其他节点继续执行。
1.3. 支持动态添加和删除任务。
1.4. 支持添加机器ip黑名单。
1.5. 支持手动执行任务。
2. 使用方法
2.1. 配置maven依赖,pom.xml配置如下:
<dependency> <groupId>cn.uncode</groupId> <artifactId>uncode-schedule</artifactId> <version>0.8.0</version></dependency>
2.2. schedule.properties配置
这里主要配置固定值,而不是系统自动生成的,目前可配置机器编码,配置如下:
#uncode.schedule.server.code=0000000001
2.3. 定时任务的spring配置,applicationContext.xml配置如下:
ScheduleManager配置
<bean id="zkScheduleManager" class="cn.uncode.schedule.ZKScheduleManager" init-method="init"> <property name="zkConfig"> <map> <entry key="zkConnectString" value="192.168.7.149:2181" /> <entry key="rootPath" value="/uncode/schedule" /> <entry key="zkSessionTimeout" value="60000" /> <entry key="userName" value="ScheduleAdmin" /> <entry key="password" value="password" /> <entry key="autoRegisterTask" value="true" /> <entry key="isCheckParentPath" value="true" /> <entry key="ipBlacklist" value="192.168.7.231" /> </map> </property></bean>
spring task配置
<task:scheduled-tasks scheduler="zkScheduleManager"> <task:scheduled ref="simpleTask" method="print" cron="0/30 * * * * ?" /></task:scheduled-tasks>
待执行任务类
@Componentpublic class SimpleTask { private static int i = 0; private Logger log = LoggerFactory.getLogger(SimpleTask.class); public void print() { log.info("===========print start!========="); log.info("print:"+i);i++; log.info("===========print end !========="); } }
从上面的配置信息中可以看出,使用框架Uncode-Schedule可以很简单的实现定时任务的分布式。从代码上看,和原来的spring task或quartz任务写法完全一样。
关键点是,每个定时任务配置的调度器是uncode-schedule框架自定义的调度器 cn.uncode.schedule.ZKScheduleManager
。上面是基于xml的配置,同样的,基于注解的配置是<task:annotation-driven scheduler="zkScheduleManager" />
,详细的配置方式可以参考uncode-schedule-learn,或者uncode-schedule。
3. 源码分析
从上面的Uncode-Schedule框架的使用和功能来看,源码分析应该有5个入口:
类
cn.uncode.schedule.ZKScheduleManager
的init
方法;类
cn.uncode.schedule.ZKScheduleManager
的定时任务初始化;类
cn.uncode.schedule.ZKScheduleManager
的心跳检测hearBeatTimer
;控制管理类
cn.uncode.schedule.ConsoleManager
;对外暴露的连个servlet接口
ManagerServlet
和ManualServlet
;
下面按照谁许依次进行源码分析:
3.1. 类 cn.uncode.schedule.ZKScheduleManager
的 init
方法
该方法的主要作用是,将配置文件中的数据加载进内存,连接zookeeper,校验zookeeper的连接状态,注册任务服务器,计算统一时间,启动心跳检测任务。
init方法的代码如下:
public void init() throws Exception { Properties properties = new Properties(); for (Map.Entry<String, String> e : this.zkConfig.entrySet()) { properties.put(e.getKey(), e.getValue()); } this.init(properties); }
将xml配置文件中的配置信息加载进properties变量,然后去进一步初始化。
public void init(Properties p) throws Exception { if (this.initialThread != null) { this.initialThread.stopThread(); } this.initLock.lock(); try { this.scheduleDataManager = null; if (this.zkManager != null) { this.zkManager.close(); } //连接zookeeper this.zkManager = new ZKManager(p); this.errorMessage = "Zookeeper connecting ......" + this.zkManager.getConnectStr(); initialThread = new InitialThread(this); initialThread.setName("ScheduleManager-initialThread"); initialThread.start(); } finally { this.initLock.unlock(); } }
在代码中通过this.zkManager = new ZKManager(p);
和zookeeper建立连接,然后会启动一个初始化线程,这个线程的作业主要是等待连接zookeeper成功之后,进一步初始化之后的注册服务器等,初始化线程的代码如下:
class InitialThread extends Thread { private transient Logger log = LoggerFactory.getLogger(InitialThread.class); ZKScheduleManager sm; public InitialThread(ZKScheduleManager sm) { this.sm = sm; } boolean isStop = false; public void stopThread() { this.isStop = true; } @Override public void run() { sm.initLock.lock(); try { int count = 0; while (!sm.zkManager.checkZookeeperState()) { count = count + 1; if (count % 50 == 0) { sm.errorMessage = "Zookeeper connecting ......" + sm.zkManager.getConnectStr() + " spendTime:" + count * 20 + "(ms)"; log.error(sm.errorMessage); } Thread.sleep(20); if (this.isStop) { return; } } sm.initialData(); } catch (Throwable e) { log.error(e.getMessage(), e); } finally { sm.initLock.unlock(); } } }
看线程的 run
方法,while
循环中检测是否连接成功zookeeper,连接成功之后,调用 sm.initialData();
真正的初始化 ZKScheduleManager
,初始化的代码如下:
public void initialData() throws Exception { //首先进行了框架的版本兼容性校验 this.zkManager.initial(); this.scheduleDataManager = new ScheduleDataManager4ZK(this.zkManager); if (this.start) { // 注册调度管理器 this.scheduleDataManager.registerScheduleServer(this.currenScheduleServer); if (hearBeatTimer == null) { hearBeatTimer = new Timer("ScheduleManager-" + this.currenScheduleServer.getUuid() + "-HearBeat"); } hearBeatTimer.schedule(new HeartBeatTimerTask(this), 2000, this.timerInterval); } }
代码中首先进行了版本兼容性校验,然后将自身作为一个调度服务器注册到管理器中,最后启动检测调度器本身的心跳任务。心跳检测的任务在下一个小节重点分析,这里重点看一下注册调度管理器,代码如下:
@Override public void registerScheduleServer(ScheduleServer server) throws Exception { if(server.isRegister()){ throw new Exception(server.getUuid() + " 被重复注册"); } //clearExpireScheduleServer(); String realPath; //此处必须增加UUID作为唯一性保障 StringBuffer id = new StringBuffer(); id.append(server.getIp()).append("$") .append(UUID.randomUUID().toString().replaceAll("-", "").toUpperCase()); String serverCode = ScheduleUtil.getServerCode(); if(serverCode != null){ //如果配置文件schedule.properties中配置server code String zkServerPath = pathServer + "/" + id.toString() + "$" + serverCode; realPath = this.getZooKeeper().create(zkServerPath, null, this.zkManager.getAcl(),CreateMode.PERSISTENT); }else{ String zkServerPath = pathServer + "/" + id.toString() +"$"; realPath = this.getZooKeeper().create(zkServerPath, null, this.zkManager.getAcl(),CreateMode.PERSISTENT_SEQUENTIAL); } server.setUuid(realPath.substring(realPath.lastIndexOf("/") + 1)); Timestamp heartBeatTime = new Timestamp(getSystemTime()); server.setHeartBeatTime(heartBeatTime); String valueString = this.gson.toJson(server); this.getZooKeeper().setData(realPath,valueString.getBytes(),-1); server.setRegister(true); }
将调度服务器信息注册到zookeeper中,服务器信息在zk上的节点是由 ip$UUID$serverCode
组成,存储在目录{rootPath}/server
下,例如, 192.168.7.231$B6A47BA82F4C44389D8D066F571D51D8$1000000001
。其中serverCode有两个来源,一是配置文件schedule.properties
中的 uncode.schedule.server.code
,另一个是由zk的持久化顺序节点生产,这个数值关系到分布式系统中leader节点的选取,因此做成可配置的,从而控制leader节点的选取,选leader节点的算法将会在心跳检测中详细介绍。
并且zk中server路径下的每一个服务器节点中都存储有相关数据,主要数据包括注册时间、最后一次心跳时间、ip、UUID等。
3.2. 类cn.uncode.schedule.ZKScheduleManager的定时任务初始化
这里主要介绍分布式任务调度器初始化完毕之后,定时任务启动时的任务注册和任务启动的代码。
类cn.uncode.schedule.ZKScheduleManager
继承了类 org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
,它又实现了接口org.springframework.scheduling.TaskScheduler
,重写以下接口来实现在任务调度的同时将定时任务的信息注册到zookeeper中。
ScheduledFuture<?> schedule(Runnable task, Trigger trigger); ScheduledFuture<?> schedule(Runnable task, Date startTime); ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Date startTime, long period); ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period); ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Date startTime, long delay); ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long delay);
重写之后的源代码如下:
@Overridepublic ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period) { TaskDefine taskDefine = getTaskDefine(task); LOGGER.info("spring task init------taskName:{}, period:{}", taskDefine.stringKey(), period); taskDefine.setPeriod(period); addTask(task, taskDefine); return super.scheduleAtFixedRate(taskWrapper(task), period); }public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) { TaskDefine taskDefine = getTaskDefine(task); if(trigger instanceof CronTrigger){ CronTrigger cronTrigger = (CronTrigger)trigger; taskDefine.setCronExpression(cronTrigger.getExpression()); LOGGER.info("spring task init------trigger:" + cronTrigger.getExpression()); } addTask(task, taskDefine); return super.schedule(taskWrapper(task), trigger); }public ScheduledFuture<?> schedule(Runnable task, Date startTime) { TaskDefine taskDefine = getTaskDefine(task); LOGGER.info("spring task init------taskName:{}, period:{}", taskDefine.stringKey(), startTime); taskDefine.setStartTime(startTime); addTask(task, taskDefine); return super.schedule(taskWrapper(task), startTime); }public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Date startTime, long period) { TaskDefine taskDefine = getTaskDefine(task); LOGGER.info("spring task init------taskName:{}, period:{}", taskDefine.stringKey(), period); taskDefine.setStartTime(startTime); taskDefine.setPeriod(period); addTask(task, taskDefine); return super.scheduleAtFixedRate(taskWrapper(task), startTime, period); }public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Date startTime, long delay) { TaskDefine taskDefine = getTaskDefine(task); LOGGER.info("spring task init------taskName:{}, delay:{}", taskDefine.stringKey(), delay); taskDefine.setStartTime(startTime); taskDefine.setPeriod(delay); taskDefine.setType(TaskDefine.TASK_TYPE_QSD); addTask(task, taskDefine); return super.scheduleWithFixedDelay(taskWrapper(task), startTime, delay); }public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long delay) { TaskDefine taskDefine = getTaskDefine(task); LOGGER.info("spring task init------taskName:{}, delay:{}", taskDefine.stringKey(), delay); taskDefine.setPeriod(delay); taskDefine.setType(TaskDefine.TASK_TYPE_QSD); addTask(task, taskDefine); return super.scheduleWithFixedDelay(taskWrapper(task), delay); }
主要是在任务调度之前,通过private TaskDefine getTaskDefine(Runnable task);
获取任务的详细信息,然后通过private void addTask(Runnable task, TaskDefine taskDefine)
将其存储到zookeeper中。
另外一个关键点是,所有的task都经过了 taskWrapper
的包装,先看代码:
/** * 将Spring的定时任务进行包装,决定任务是否在本机执行。 * @param task * @return */private Runnable taskWrapper(final Runnable task){ return new Runnable(){ public void run(){ Method targetMethod = null; if(task instanceof ScheduledMethodRunnable){ ScheduledMethodRunnable uncodeScheduledMethodRunnable = (ScheduledMethodRunnable)task; targetMethod = uncodeScheduledMethodRunnable.getMethod(); }else{ org.springframework.scheduling.support.ScheduledMethodRunnable springScheduledMethodRunnable = (org.springframework.scheduling.support.ScheduledMethodRunnable)task; targetMethod = springScheduledMethodRunnable.getMethod(); } String[] beanNames = applicationcontext.getBeanNamesForType(targetMethod.getDeclaringClass()); if(null != beanNames && StringUtils.isNotEmpty(beanNames[0])){ String name = ScheduleUtil.getTaskNameFormBean(beanNames[0], targetMethod.getName()); boolean isOwner = false; try { if(!isScheduleServerRegister){ Thread.sleep(1000); } if(zkManager.checkZookeeperState()){ isOwner = scheduleDataManager.isOwner(name, currenScheduleServer.getUuid()); isOwnerMap.put(name, isOwner); }else{ // 如果zk不可用,使用历史数据 if(null != isOwnerMap){ isOwner = isOwnerMap.get(name); } } if(isOwner){ task.run(); scheduleDataManager.saveRunningInfo(name, currenScheduleServer.getUuid()); LOGGER.info("Cron job has been executed."); } } catch (Exception e) { LOGGER.error("Check task owner error.", e); } } } }; }
这里主要控制定时任务的执行,在执行时,需要检测该任务是否属于该服务器。并且考虑到zookeeper不可用的情况,如果不可用查看缓存的任务归属关系。
3.3. 类cn.uncode.schedule.ZKScheduleManager的心跳检测hearBeatTimer
在分布式系统中心跳检测任务是很重要的,负责整个分布式系统的稳定性和健壮性。在3.1.节中的代码中我们看到,心跳检测的定时任务调度代码 hearBeatTimer.schedule(new HeartBeatTimerTask(this), 2000, this.timerInterval);
启动延迟2秒执行,心跳间隔2秒。心跳检测任务 HeartBeatTimerTask
的代码如下:
class HeartBeatTimerTask extends java.util.TimerTask { private transient final Logger log = LoggerFactory.getLogger(HeartBeatTimerTask.class); ZKScheduleManager manager; public HeartBeatTimerTask(ZKScheduleManager aManager) { manager = aManager; } public void run() { try { Thread.currentThread().setPriority(Thread.MAX_PRIORITY); manager.refreshScheduleServer(); } catch (Exception ex) { log.error(ex.getMessage(), ex); } } }
从以上代码中可以看到,心跳检测通过 manager.refreshScheduleServer();
不停在刷新调度服务器信息,代码是:
/** * 1. 定时向数据配置中心更新当前服务器的心跳信息。 如果发现本次更新的时间如果已经超过了,服务器死亡的心跳周期,则不能在向服务器更新信息。 * 而应该当作新的服务器,进行重新注册。 * 2. 任务分配 * 3. 检查任务是否属于本机,是否添加到调度器 * * @throws Exception */public void refreshScheduleServer() throws Exception { try { // 更新或者注册服务器信息 rewriteScheduleInfo(); // 如果任务信息没有初始化成功,不做任务相关的处理 if (!this.isScheduleServerRegister) { return; } // 重新分配任务 this.assignScheduleTask(); // 检查本地任务 this.checkLocalTask(); } catch (Throwable e) { // 清除内存中所有的已经取得的数据和任务队列,避免心跳线程失败时候导致的数据重复 this.clearMemoInfo(); if (e instanceof Exception) { throw (Exception) e; } else { throw new Exception(e.getMessage(), e); } } }
进入到方法之后看到,心跳检测任务主要负责:
方法
rewriteScheduleInfo();
的功能是,定时向数据配置中心zk更新当前服务器的心跳信息,如果更新失败,重新注册调度服务器信息(在3.1节中已经介绍过了,就是方法scheduleDataManager.registerScheduleServer
);方法
assignScheduleTask();
的功能是,定时任务的分配,分配任务的时候会校验该节点是否是leader节点,因为只有leader节点才能分配任务;在分配任务的时候启用了服务器ip黑名单,在黑名单列表中的机器不参与任务分配;检查本地的定时任务,添加调度器;该功能是检查是否有通过控制台添加uncode task 类型的定时任务,如果有的话启动该定时任务;这是一种自定义的定时任务类型,任务的启动方式也是自定义的,主要方法在类
DynamicTaskManager
中;
下面看几个关键步骤的代码:首先是leader节点的选择算法代码,
private String getLeader(List<String> serverList){ if(serverList == null || serverList.size() ==0){ return ""; } long no = Long.MAX_VALUE; long tmpNo = -1; String leader = null; for(String server:serverList){ tmpNo =Long.parseLong( server.substring(server.lastIndexOf("$")+1)); if(no > tmpNo){ no = tmpNo; leader = server; } } return leader; }
从代码可以看出,选择leader节点的算法是,取serverCode最小的服务器为leader。这种方法的好处是,由于serverCode是递增的,再新增服务器的时候,leader节点不会变化,比较稳定,算法又简单。
3.4. 控制管理类cn.uncode.schedule.ConsoleManager
在该类的功能主要是对外提供的是一些操作任务和数据的方法,包括注册在zk上的定时任务数据的增、删、查;以及定时任务的执行入口。主要代码如下:
public static void addScheduleTask(TaskDefine taskDefine) throws Exception{ ConsoleManager.getScheduleManager().getScheduleDataManager().addTask(taskDefine); }public static void delScheduleTask(TaskDefine taskDefine) { try { ConsoleManager.scheduleManager.getScheduleDataManager().delTask(taskDefine); } catch (Exception e) { log.error(e.getMessage(), e); } }public static List<TaskDefine> queryScheduleTask() { List<TaskDefine> taskDefines = new ArrayList<TaskDefine>(); try { List<TaskDefine> tasks = ConsoleManager.getScheduleManager().getScheduleDataManager().selectTask(); taskDefines.addAll(tasks); } catch (Exception e) { log.error(e.getMessage(), e); } return taskDefines; }public static boolean isExistsTask(TaskDefine taskDefine) throws Exception{ return ConsoleManager.scheduleManager.getScheduleDataManager().isExistsTask(taskDefine); }/** * 手动执行定时任务 * @param task */public static void runTask(TaskDefine task) throws Exception{ Object object = null; if (StringUtils.isNotEmpty(task.getTargetBean())) { object = ZKScheduleManager.getApplicationcontext().getBean(task.getTargetBean()); } if (object == null) { log.error("任务名称 = [{}]---------------未启动成功,targetBean不存在,请检查是否配置正确!!!", task.stringKey()); throw new Exception("targetBean:"+task.getTargetBean()+"不存在"); } Method method = null; try { if(StringUtils.isNotEmpty(task.getParams())){ method = object.getClass().getDeclaredMethod(task.getTargetMethod(), String.class); }else{ method = object.getClass().getDeclaredMethod(task.getTargetMethod()); } } catch (Exception e) { log.error(String.format("定时任务bean[%s],method[%s]初始化失败.", task.getTargetBean(), task.getTargetMethod()), e); throw new Exception("定时任务:"+task.stringKey()+"初始化失败"); } if (method != null) { try { if(StringUtils.isNotEmpty(task.getParams())){ method.invoke(object, task.getParams()); }else{ method.invoke(object); } } catch (Exception e) { log.error(String.format("定时任务bean[%s],method[%s]调用失败.", task.getTargetBean(), task.getTargetMethod()), e); throw new Exception("定时任务:"+task.stringKey()+"调用失败"); } } log.info("任务名称 = [{}]----------启动成功", task.stringKey()); }
3.5. 对外暴露的连个servlet接口ManagerServlet
和ManualServlet
servlet ManagerServlet
是一个简单管理界面,ManualServlet
是一个手动执行定时任务的接口;使用方法是要在项目中的web.xml
中配置响应的servlet,配置文件代码如下:
<!-- 配置 uncode schedule 管理后台 --><servlet> <servlet-name>UncodeSchedule</servlet-name> <servlet-class>cn.uncode.schedule.web.ManagerServlet</servlet-class></servlet><servlet-mapping> <servlet-name>UncodeSchedule</servlet-name> <url-pattern>/uncode/schedule</url-pattern></servlet-mapping><!-- 配置 uncode schedule 手动执行器 --><servlet> <servlet-name>ScheduleManual</servlet-name> <servlet-class>cn.uncode.schedule.web.ManualServlet</servlet-class></servlet><servlet-mapping> <servlet-name>ScheduleManual</servlet-name> <url-pattern>/schedule/manual</url-pattern></servlet-mapping>
结束语,源代码分析结束,uncode-schedule分布式定时任务框架实现的主要功能都已覆盖到,有问题的请留言!
作者:rabbitGYK
链接:https://www.jianshu.com/p/4a7eb40f6852
共同学习,写下你的评论
评论加载中...
作者其他优质文章