XXL-Job源碼分析(I)--調度中心啓動

前言

XXL-Job是一個輕量的分佈式任務調度平臺,任務的定時調度管理是基於開源定時任務調度框架Quarts來實現的,任務的調度執行採用註冊和RPC的方式實現,將任務的管理和執行進行了分離。我們先來了解一下調度中心的源碼,調度中心主要負責任務的管理,本身並不複雜任務的業務邏輯,任務觸發後,調度中心根據配置的相應規則將任務分配到執行器,執行器處理相應的業務邏輯。

調度中心啓動流程

1、初始化任務調度器

XxlJobDynamicScheduler對Quarts的調度器進行封裝,並在XxlJobDynamicSchedulers實例化後運行start方法,完成調度中心的啓動。

XxlJobDynamicSchedulerConfig.java


    @Bean
    public SchedulerFactoryBean getSchedulerFactoryBean(DataSource dataSource){

        SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
        schedulerFactory.setDataSource(dataSource);
        schedulerFactory.setAutoStartup(true);                  // 自動啓動
        schedulerFactory.setStartupDelay(20);                   // 延時啓動,應用啓動成功後在啓動
        schedulerFactory.setOverwriteExistingJobs(true);        // 覆蓋DB中JOB:true、以數據庫中已經存在的爲準:false
        schedulerFactory.setApplicationContextSchedulerContextKey("applicationContext");
        schedulerFactory.setConfigLocation(new ClassPathResource("quartz.properties"));

        return schedulerFactory;
    }

    @Bean(initMethod = "start", destroyMethod = "destroy")
    public XxlJobDynamicScheduler getXxlJobDynamicScheduler(SchedulerFactoryBean schedulerFactory){
        // 通過注入的調度器工廠生成調度器
        Scheduler scheduler = schedulerFactory.getScheduler();

        XxlJobDynamicScheduler xxlJobDynamicScheduler = new XxlJobDynamicScheduler();
        xxlJobDynamicScheduler.setScheduler(scheduler);

        return xxlJobDynamicScheduler;
    }

2、start方法

由系統的java config類可知,調度中心的啓動的初始化工作由start方法實現,start方法源碼如下,主要有三個步驟:
1)、啓動執行器註冊發現監聽線程,該線程負責執行器實例的註冊與下線;
2)、啓動任務執行失敗監聽線程,負責處理執行失敗的任務;
3)、初始化RPC,調度中心與執行器通過RPC方式通信。
同時可以看到JobRegistryMonitorHelper和JobFailMonitorHelper都是單例模式實現。

    public void start() throws Exception {
        // valid 驗證Quarts調度器是否爲空
        Assert.notNull(scheduler, "quartz scheduler is null");

        // init i18n 國際化
        initI18n();

        // admin registry monitor run 自動註冊線程,完成執行器的自動發現和註冊
        JobRegistryMonitorHelper.getInstance().start();

        // admin monitor run 啓動任務執行失敗監控線程
        JobFailMonitorHelper.getInstance().start();

        // admin-server 初始化RpcProvider
        initRpcProvider();

        logger.info(">>>>>>>>> init xxl-job admin success.");
    }
3、執行器註冊監聽線程

這裏執行器註冊的方式是,執行器啓動後會以同樣的心跳檢測間隔在數據庫中更新自己的信息,然後調度中心的監聽線程從數據庫中獲取執行器的註冊信息,更新到內存中。

private Thread registryThread;
	private volatile boolean toStop = false;
	public void start(){
		registryThread = new Thread(new Runnable() {
			@Override
			public void run() {
				while (!toStop) {
					try {
						// 自動註冊任務組,從數據庫中獲取保存的任務組,這裏的任務組代表一組執行器
						List<XxlJobGroup> groupList = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0);
						if (groupList!=null && !groupList.isEmpty()) {

							// 移除已經下線的執行器和調度中心節點,心跳檢測間隔默認爲10s一次,連續檢測三次節點不在線,就移除,刪除數據庫中節點的信息
							XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(RegistryConfig.DEAD_TIMEOUT);

							// 從數據庫中獲取在線節點的ip地址,並刷新內存中數據
							HashMap<String, List<String>> appAddressMap = new HashMap<String, List<String>>();
							List<XxlJobRegistry> list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findAll(RegistryConfig.DEAD_TIMEOUT);
							if (list != null) {
								for (XxlJobRegistry item: list) {
									if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) {
										String appName = item.getRegistryKey();
										List<String> registryList = appAddressMap.get(appName);
										if (registryList == null) {
											registryList = new ArrayList<String>();
										}

										if (!registryList.contains(item.getRegistryValue())) {
											registryList.add(item.getRegistryValue());
										}
										appAddressMap.put(appName, registryList);
									}
								}
							}

							// 刷新每個任務組中,在線節點的IP信息
							for (XxlJobGroup group: groupList) {
								List<String> registryList = appAddressMap.get(group.getAppName());
								String addressListStr = null;
								if (registryList!=null && !registryList.isEmpty()) {
									Collections.sort(registryList);
									addressListStr = StringUtils.join(registryList, ",");
								}
								group.setAddressList(addressListStr);
								XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group);
							}
						}
					} catch (Exception e) {
						if (!toStop) {
							logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e);
						}
					}
					// 心跳檢測,十秒一次,連續三次不在線就判斷爲下線
					try {
						TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
					} catch (InterruptedException e) {
						if (!toStop) {
							logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e);
						}
					}
				}
				logger.info(">>>>>>>>>>> xxl-job, job registry monitor thread stop");
			}
		});
		// 把線程設置爲守護線程
		registryThread.setDaemon(true);
		registryThread.setName("xxl-job, admin JobRegistryMonitorHelper");
		// 啓動線程
		registryThread.start();
	}

4、任務執行失敗監聽

這部分的實現原理和執行器註冊監聽原理一樣,從數據庫中獲取執行失敗的任務,然後進行相應的處理。

public void start(){
		monitorThread = new Thread(new Runnable() {

			@Override
			public void run() {

				// monitor
				while (!toStop) {
					try {

						List<Integer> failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findFailJobLogIds(1000);
						if (failLogIds!=null && !failLogIds.isEmpty()) {
							for (int failLogId: failLogIds) {

								// 日誌加鎖
								int lockRet = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, 0, -1);
								if (lockRet < 1) {
									continue;
								}
								// 從數據庫中獲取執行失敗的任務日誌
								XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(failLogId);
								// 從數據庫中讀取失敗任務的信息
								XxlJobInfo info = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(log.getJobId());

								// 1、失敗重試,若配置了失敗重試,則重新觸發任務
								if (log.getExecutorFailRetryCount() > 0) {
									JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), null);
									String retryMsg = "<br><br><span style=\"color:#F39C12;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_type_retry") +"<<<<<<<<<<< </span><br>";
									log.setTriggerMsg(log.getTriggerMsg() + retryMsg);
									XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(log);
								}

								// 2、失敗告警,根據配置決定是否告警
								int newAlarmStatus = 0;		// 告警狀態:0-默認、-1=鎖定狀態、1-無需告警、2-告警成功、3-告警失敗
								if (info!=null && info.getAlarmEmail()!=null && info.getAlarmEmail().trim().length()>0) {
									boolean alarmResult = true;
									try {
										alarmResult = failAlarm(info, log);
									} catch (Exception e) {
										alarmResult = false;
										logger.error(e.getMessage(), e);
									}
									newAlarmStatus = alarmResult?2:3;
								} else {
									newAlarmStatus = 1;
								}
								// 釋放日誌鎖

								XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, -1, newAlarmStatus);
							}
						}

						TimeUnit.SECONDS.sleep(10);
					} catch (Exception e) {
						if (!toStop) {
							logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:{}", e);
						}
					}
				}

				logger.info(">>>>>>>>>>> xxl-job, job fail monitor thread stop");

			}
		});
		monitorThread.setDaemon(true);
		monitorThread.setName("xxl-job, admin JobFailMonitorHelper");
		monitorThread.start();
	}
5、Rpc初始化

調度平臺的Rpc服務是基於XXL-RPC實現,添加了一個服務AdminBiz,該服務負責執行器的註冊及移除。
前面提到的執行器註冊監聽是通過數據庫操作實現的,在執行器註冊時便是調用AdminBiz這個接口將自己的註冊信息寫入數據庫的。

private void initRpcProvider(){
        // 工廠初始化
        XxlRpcProviderFactory xxlRpcProviderFactory = new XxlRpcProviderFactory();
        xxlRpcProviderFactory.initConfig(
                NetEnum.NETTY_HTTP,
                Serializer.SerializeEnum.HESSIAN.getSerializer(),
                null,
                0,
                XxlJobAdminConfig.getAdminConfig().getAccessToken(),
                null,
                null);

        // 添加服務
        xxlRpcProviderFactory.addService(AdminBiz.class.getName(), null, XxlJobAdminConfig.getAdminConfig().getAdminBiz());

        // servlet handler
        servletServerHandler = new ServletServerHandler(xxlRpcProviderFactory);
    }

總結

總的來說,調度中心在啓動時主要做了三件事,第一、啓動註冊監聽,通過心跳檢測管理執行器節點的上線及下線,第二、啓動任務執行失敗監聽,對失敗任務進行處理,最後啓動RPCProvider,提供執行器註冊服務

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章