Uncode-Schedule框架原始碼分析
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" />
,詳細的配置方式可以參考,或者。
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
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1600/viewspace-2820546/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- MJRefresh原始碼框架分析原始碼框架
- Uber RIBs框架原始碼分析框架原始碼
- Java 集合框架------ArrayList原始碼分析Java框架原始碼
- Java容器類框架分析(1)ArrayList原始碼分析Java框架原始碼
- Java容器類框架分析(2)LinkedList原始碼分析Java框架原始碼
- Java容器類框架分析(5)HashSet原始碼分析Java框架原始碼
- Java類集框架 —— ArrayList原始碼分析Java框架原始碼
- Java類集框架 —— LinkedList原始碼分析Java框架原始碼
- Android Hook框架Xposed原理與原始碼分析AndroidHook框架原始碼
- Java類集框架 —— HashSet、LinkedHashSet原始碼分析Java框架原始碼
- 圖片載入框架Picasso - 原始碼分析框架原始碼
- Spark RPC框架原始碼分析(一)簡述SparkRPC框架原始碼
- 圖片載入框架Picasso原始碼分析框架原始碼
- Android 外掛化框架 DynamicLoadApk 原始碼分析Android框架APK原始碼
- workerman 框架原始碼核心分析和註解框架原始碼
- 如何優雅的分析 ThinkPHP 框架原始碼PHP框架原始碼
- 原始碼分析:同步基礎框架——AbstractQueuedSynchronizer(AQS)原始碼框架AQS
- Spark RPC框架原始碼分析(三)Spark心跳機制分析SparkRPC框架原始碼
- Retrofit原始碼分析三 原始碼分析原始碼
- Seata 分散式事務框架 TCC 模式原始碼分析分散式框架模式原始碼
- Rxjava 2.x 原始碼系列 - 基礎框架分析RxJava原始碼框架
- 集合原始碼分析[2]-AbstractList 原始碼分析原始碼
- 集合原始碼分析[3]-ArrayList 原始碼分析原始碼
- Guava 原始碼分析之 EventBus 原始碼分析Guava原始碼
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- 集合原始碼分析[1]-Collection 原始碼分析原始碼
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- 美團外賣開源路由框架 WMRouter 原始碼分析路由框架原始碼
- Spark RPC框架原始碼分析(二)RPC執行時序SparkRPC框架原始碼
- Android 記憶體快取框架 LruCache 的原始碼分析Android記憶體快取框架原始碼
- Kafka原始碼分析(四) - Server端-請求處理框架Kafka原始碼Server框架
- 微前端框架 之 qiankun 從入門到原始碼分析前端框架原始碼
- 以太坊原始碼分析(36)ethdb原始碼分析原始碼
- 以太坊原始碼分析(38)event原始碼分析原始碼
- 以太坊原始碼分析(41)hashimoto原始碼分析原始碼
- 以太坊原始碼分析(43)node原始碼分析原始碼
- 以太坊原始碼分析(51)rpc原始碼分析原始碼RPC
- 以太坊原始碼分析(52)trie原始碼分析原始碼