Detailed explanation of xxl-job scheduling center and executor source code

Article directory

Introduction

xxl-job is a distributed task scheduling platform developed based on java. The integration is very simple. After downloading the project from the official website, the scheduling center configures the mysql data source, imports the tables required by default into the database, and the scheduling center project is packaged into a jar package, start it directly, and the scheduling platform is created. The executor is a specific business development project. You only need to introduce xxl-job-core dependencies, configure the dispatch center address, execute the log storage directory, create the executor object, and use @XxlJob to define the specific tasks for scheduling. Realize the visual operation mode of task scheduling, and the operation is very simple. Here, the source code analysis of important processes is carried out. For specific usage details, please refer to the official website .

dispatch center

​ The scheduling center is a platform for task management, task execution status, execution results, and execution log monitoring. It is a web project, and users can easily manage tasks. Support email alarm, support CRON and fixed speed two scheduling methods, support Bean, shell script, php and other operating modes, support random, polling, failover and other routing strategies, support joint execution of subtasks, support ignore , Immediately execute two scheduling expiration strategies, and support three blocking processing strategies: stand-alone serial, discard subsequent scheduling, and overwrite previous scheduling.

1. Program startup initialization

​ After the program starts, it will do a lot of resource initialization and create the required daemon threads. The entry classes for resource initialization are the JobAlarmer class and the XxlJobAdminConfig class. Let's look at the initialization process from these two classes.

1. Initialize the entry class

​ The JobAlarmer class and the XxlJobAdminConfig class will be used as initialization entry classes because they are modified by @Component, in spring

The container is registered as a Bean object and implements the InitializingBean interface. This interface has a method afterPropertiesSet(). After the Bean is initialized and the parameters are successfully injected, the afterPropertiesSet (after property setting) method will be called, and the resource initialization is performed in this method. The JobAlarmer class also implements the ApplicationContextAware interface. This interface has a method setApplicationContext, which will set the spring container context to this method. We can define a variable to receive the ApplicationContext, so that we can get the bean object registered in the spring container. The JobAlarmer class is to obtain the defined alarm class from the spring container. When an alarm is required, all alarm classes are called to execute the alarm method. The alarm class supports custom extensions. The extension method only needs to implement the JobAlarm interface, and the custom extension class Set it as a Bean (@Component decoration), rewrite the alarm method, and complete the alarm processing of the specific extension method.

​ Look at the source code of the JobAlarmer class:

@Component
public class JobAlarmer implements ApplicationContextAware, InitializingBean {
    
    
    private static Logger logger = LoggerFactory.getLogger(JobAlarmer.class);

    private ApplicationContext applicationContext;
    private List<JobAlarm> jobAlarmList;    //存放报警的实现类,可以进行扩展,实现JobAlarm接口,并把实现类注册为bean即可

    //实现ApplicationContextAware接口,获取上下文,得到加载到spring容器中的所有bean对象
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    
    
        this.applicationContext = applicationContext;
    }

    //实现了InitializingBean接口,在Bean初始化完并把参数注入成功后会调用afterPropertiesSet()
    @Override
    public void afterPropertiesSet() throws Exception {
    
    
        //从spring容器中获取JobAlarm类型的bean,并存放到list集合中
        Map<String, JobAlarm> serviceBeanMap = applicationContext.getBeansOfType(JobAlarm.class);
        if (serviceBeanMap != null && serviceBeanMap.size() > 0) {
    
    
            jobAlarmList = new ArrayList<JobAlarm>(serviceBeanMap.values());
        }
    }

    /**
     * job alarm
     *  发送预警邮件
     * @param info
     * @param jobLog
     * @return
     */
    public boolean alarm(XxlJobInfo info, XxlJobLog jobLog) {
    
    

        boolean result = false;
        //报警的集合类不为空
        if (jobAlarmList!=null && jobAlarmList.size()>0) {
    
    
            result = true;  // success means all-success
            for (JobAlarm alarm: jobAlarmList) {
    
    
                boolean resultItem = false;
                try {
    
    
                    //执行报警方法
                    resultItem = alarm.doAlarm(info, jobLog);
                } catch (Exception e) {
    
    
                    logger.error(e.getMessage(), e);
                }
                if (!resultItem) {
    
    
                    result = false;
                }
            }
        }

        return result;
    }

}

Take a look at part of the source code of the XxlJobAdminConfig class:

@Component
public class XxlJobAdminConfig implements InitializingBean, DisposableBean {
    
    


    private XxlJobScheduler xxlJobScheduler;

    //实现了InitializingBean接口,在Bean初始化完并把参数注入成功后会调用afterPropertiesSet()
    @Override
    public void afterPropertiesSet() throws Exception {
    
    

        xxlJobScheduler = new XxlJobScheduler();
        //初始化调度中心资源
        xxlJobScheduler.init();
    }
}

2. Initialize I18n

​ The text of the system web page supports three types: en (English), zh_CN (Chinese), zh_TC (traditional Chinese), which type to use is specified in the application.propreties configuration file, and the configuration items are as follows:

xxl.job.i18n=zh_CN

The entry to initialize resources is the afterPropertiesSet() method of the XxlJobAdminConfig class. This method creates the XxlJobScheduler class and executes its init method. In its init method, there is the method initI18n() for initializing i18n. Let’s look at the source code of initI18n():

    //初始化i18n,用于不同语言的字符显示
    private void initI18n(){
    
    
        //对阻塞处理策略枚举类重新设置title值,连带着初始化了I18n
        for (ExecutorBlockStrategyEnum item:ExecutorBlockStrategyEnum.values()) {
    
    
            //根据选择的i18n类型,从i18n配置文件中根据key加载对应的文本
            item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name())));
        }
    }

This method resets the title value for the blocking processing strategy enumeration class. The title value needs to be matched from the i18n dictionary. When reading the I18n dictionary, i18n is initialized. Look at the source code of the method to get the value from I18n according to the key:

    public static String getString(String key) {
    
    
        //加载i18n的字典文件,从此字典文件中根据key获取value值
        return loadI18nProp().getProperty(key);
    }
    
    private static Properties prop = null;
    //根据选择的i18n类型,加载对应的配置文件
    public static Properties loadI18nProp(){
    
    
        if (prop != null) {
    
    
            return prop;
        }
        try {
    
    
            // build i18n prop
            //获取配置的i18n类型
            String i18n = XxlJobAdminConfig.getAdminConfig().getI18n();
            //根据类型拼接出需要的字典文件名
            String i18nFile = MessageFormat.format("i18n/message_{0}.properties", i18n);

            // load prop
            //根据文件目录加载资源
            Resource resource = new ClassPathResource(i18nFile);
            EncodedResource encodedResource = new EncodedResource(resource,"UTF-8");
            //加载properties配置文件信息
            prop = PropertiesLoaderUtils.loadProperties(encodedResource);
        } catch (IOException e) {
    
    
            logger.error(e.getMessage(), e);
        }
        return prop;
    }

Get the dictionary item Properties. If it is called for the first time, the Properties will be empty. At this time, the configured i18n type will be obtained from the configuration file, and the required dictionary file name will be spliced ​​out according to the type, and then the dictionary file will be loaded. The dictionary file is the properties type. Stored in the resources/i18n directory:
insert image description here

Then get the value from Properties according to the key. After the first loading, the value is directly taken from the prop later.

3. Initialize the fast and slow scheduling thread pool

​ In order to optimize scheduling efficiency, fast and slow scheduling thread pools are defined. The difference between fast and slow thread pools lies in the maximum number of threads and the size of the blocking queue; the maximum number of threads in the fast thread pool defaults to 200, and those less than 200 are processed as 200, and the blocking queue is 1000 ;The maximum number of threads in the slow thread pool defaults to 100, if it is less than 100, it will be processed as 100, and the blocking queue is 2000. The basis for choosing which thread pool to execute when executing task scheduling: within 1 minute, this task has 10 times more than 500 milliseconds before the scheduling is completed, use the slow thread pool for processing, otherwise use the fast thread pool for processing.

​ The entry to initialize this resource is the JobTriggerPoolHelper.toStart() method, see the source code of this toStart():

    private static JobTriggerPoolHelper helper = new JobTriggerPoolHelper();
    //初始化调度线程池
    public static void toStart() {
    
    
        //调用JobTriggerPoolHelper的start方法
        helper.start();
    }

The JobTriggerPoolHelper class creates its own object helper and calls its own start method. See the source code of the start method:

    private ThreadPoolExecutor fastTriggerPool = null;
    private ThreadPoolExecutor slowTriggerPool = null;

    public void start(){
    
    
        //初始化快的调度线程池
        fastTriggerPool = new ThreadPoolExecutor(
                10,
                XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax(),
                60L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(1000),
                new ThreadFactory() {
    
    
                    @Override
                    public Thread newThread(Runnable r) {
    
    
                        return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-fastTriggerPool-" + r.hashCode());
                    }
                });
        //初始化慢的调度线程池,与快的不同是阻塞队列的大小为2000、最大线程数
        slowTriggerPool = new ThreadPoolExecutor(
                10,
                XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax(),
                60L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(2000),
                new ThreadFactory() {
    
    
                    @Override
                    public Thread newThread(Runnable r) {
    
    
                        return new Thread(r, "xxl-job, admin JobTriggerPoolHelper-slowTriggerPool-" + r.hashCode());
                    }
                });
    }

This method initializes the fast and slow thread pool, obtains the maximum number of threads in the fast and slow thread pool from the configuration file, and specifies this value in the application.properties configuration file:

xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100

When obtaining this value, there is a minimum restriction:

    //注入配置变量
    @Value("${xxl.job.triggerpool.fast.max}")
    private int triggerPoolFastMax;

    @Value("${xxl.job.triggerpool.slow.max}")
    private int triggerPoolSlowMax;

    public int getTriggerPoolFastMax() {
    
    
        //小于200按200处理
        if (triggerPoolFastMax < 200) {
    
    
            return 200;
        }
        return triggerPoolFastMax;
    }

    public int getTriggerPoolSlowMax() {
    
    
        //小于100按100处理
        if (triggerPoolSlowMax < 100) {
    
    
            return 100;
        }
        return triggerPoolSlowMax;
    }

4. Initialize and process the executor registration or remove the thread pool + update the latest online daemon thread of the executor

​ The entry to initialize this resource is: JobRegistryHelper.getInstance().start(), which mainly initializes the thread pool for processing executor registration and removal, initializes and updates the daemon thread that automatically registers the latest online status of executors, and the heartbeat trigger mechanism (default 30 seconds, and the executor registration period is also 30 seconds), delete the executors that are longer than the heartbeat time * 3 (90 seconds) without the latest registration. Look at the source code of the start method:

	public void start(){
    
    

		// for registry or remove
		//创建处理执行器注册或者删除的线程池
		registryOrRemoveThreadPool = new ThreadPoolExecutor(
				2,
				10,
				30L,
				TimeUnit.SECONDS,
				new LinkedBlockingQueue<Runnable>(2000),
				new ThreadFactory() {
    
    
					@Override
					public Thread newThread(Runnable r) {
    
    
						return new Thread(r, "xxl-job, admin JobRegistryMonitorHelper-registryOrRemoveThreadPool-" + r.hashCode());
					}
				},
				new RejectedExecutionHandler() {
    
    
					@Override
					public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    
    
						r.run();
						logger.warn(">>>>>>>>>>> xxl-job, registry or remove too fast, match threadpool rejected handler(run now).");
					}
				});

		// for monitor
		//创建更新执行器最新在线的守护线程
		registryMonitorThread = new Thread(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				//不销毁就一直执行
				while (!toStop) {
    
    
					try {
    
    
						// auto registry group
						//只处理自动注册的执行器组
						List<XxlJobGroup> groupList = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0);
						//查询到有记录
						if (groupList!=null && !groupList.isEmpty()) {
    
    

							// remove dead address (admin/executor)
							//从执行器注册的表里面查询大于心跳时间(默认90秒)没有过注册的记录,进行删除,表示这些执行器可能已经下线了
							List<Integer> ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date());
							if (ids!=null && ids.size()>0) {
    
    
								//删除已经下线的执行器记录
								XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids);
							}

							// fresh online address (admin/executor)
							//使用集合记录当前执行器在线情况,key:执行器AppName,value:这个执行器分组下的执行器集合
							HashMap<String, List<String>> appAddressMap = new HashMap<String, List<String>>();
							//查询在心跳时间(默认90秒)内有过注册的执行器
							List<XxlJobRegistry> list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findAll(RegistryConfig.DEAD_TIMEOUT, new Date());
							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());
										}
										//key:执行器AppName,value:这个执行器分组下的执行器集合,多个执行器根据配置的appName进行分组
										appAddressMap.put(appname, registryList);
									}
								}
							}

							// fresh group address
							//刷新自动注册执行器分组里面当前在线的执行器列表
							for (XxlJobGroup group: groupList) {
    
    
								//根据key从当前在线执行器集合里面获取到某个执行器分组的在线集合
								List<String> registryList = appAddressMap.get(group.getAppname());
								String addressListStr = null;
								//执行器分组在线集合不为空,则重新设置下此执行器分组最新的在线情况;若是为空,则表示此执行器分组下已经没有在线的执行器了,则给执行器在线分组设置为null
								if (registryList!=null && !registryList.isEmpty()) {
    
    
									//排序
									Collections.sort(registryList);
									StringBuilder addressListSB = new StringBuilder();
									//使用逗号进行当前执行器分组下在线执行器的数组组织
									for (String item:registryList) {
    
    
										addressListSB.append(item).append(",");
									}
									addressListStr = addressListSB.toString();
									addressListStr = addressListStr.substring(0, addressListStr.length()-1);
								}
								group.setAddressList(addressListStr);
								group.setUpdateTime(new Date());
                                //更新执行器分组下当前在线的执行器数据
								XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group);
							}
						}
					} catch (Exception e) {
    
    
						if (!toStop) {
    
    
							logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e);
						}
					}
					try {
    
    
						//默认休眠30秒,与执行器心跳注册的时间保持一致
						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");
			}
		});
		//设置为守护线程
		registryMonitorThread.setDaemon(true);
		registryMonitorThread.setName("xxl-job, admin JobRegistryMonitorHelper-registryMonitorThread");
		//启动线程
		registryMonitorThread.start();
	}

The registryOrRemoveThreadPool thread pool is used to register or delete the registration of the executor. The registryMonitorThread daemon thread will query the task group whose registration method is automatic registration. The task group data is stored in the xxl_job_group table, and the address_type field identifies the registration method (0 automatic registration). The address_list field identifies the set of currently online executors (connected with commas), and the app_name field is used to group registered executors. Automatically registered executors need to have their own group values, and determine which task this executor belongs to according to the group values Group. After querying the automatically registered task group, get the latest executor online status from the executor automatic registry xxl_job_registry (the executor registers once every 30 seconds by default), and for executors that have not been registered for more than 90 seconds, transfer it from xxl_job_registry Delete from the table; then query the latest online status of the executor from the xxl_job_registry table (registered within 90 seconds), organize the online status according to the group value, and finally update the address_list field value of the xxl_job_group table.

​ That is to say, the registryMonitorThread daemon thread will continuously check the expired and unregistered executors with a sleep cycle of 30 seconds, delete its registration records, and then re-update the online executor set of the task group, so as to timely display the online and offline of executors Line detection.

5. Initialize the daemon threads that fail to schedule or execute monitoring tasks

​ The initialization entry is: JobFailMonitorHelper.getInstance().start(), the thread that initializes the monitoring task scheduling failure or the execution failure log of the executor, the heartbeat trigger mechanism (default 10 seconds), and the failed task, if there is an alarm email configured, it will be sent Alarm email; if the number of retries configured is greater than 0, the task will be rescheduled. Look at the source code of the start method:

	public void start(){
    
    

		//创建监控守护线程
		monitorThread = new Thread(new Runnable() {
    
    

			@Override
			public void run() {
    
    

				// monitor
				//不停止一致运行
				while (!toStop) {
    
    
					try {
    
    
                        //获取调度失败或者执行器执行失败的日志记录,alarm_status为0,告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败
						List<Long> failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findFailJobLogIds(1000);
						if (failLogIds!=null && !failLogIds.isEmpty()) {
    
    
							for (long failLogId: failLogIds) {
    
    

								// lock log
								//更新xxl_job_log表对应日志记录的alarm_status由0修改为-1
								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、fail retry monitor
								//任务若是配置了失败重试次数大于0,则进行重试调用
								if (log.getExecutorFailRetryCount() > 0) {
    
    
									//重试调用执行任务,调度方式为重试,重试次数为配置的次数减1,
									JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY, (log.getExecutorFailRetryCount()-1), log.getExecutorShardingParam(), log.getExecutorParam(), 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、fail alarm monitor
								int newAlarmStatus = 0;		// 告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败
								if (info != null) {
    
    
									//发送报警邮件
									boolean alarmResult = XxlJobAdminConfig.getAdminConfig().getJobAlarmer().alarm(info, log);
									newAlarmStatus = alarmResult?2:3;
								} else {
    
    
									newAlarmStatus = 1;
								}
                                //更新日志记录的报警邮件是否发送成功情况
								XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, -1, newAlarmStatus);
							}
						}

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

                    try {
    
    
                    	//休眠10,心跳周期为10秒
                        TimeUnit.SECONDS.sleep(10);
                    } catch (Exception e) {
    
    
                        if (!toStop) {
    
    
                            logger.error(e.getMessage(), e);
                        }
                    }

                }

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

			}
		});
		//设置为守护线程
		monitorThread.setDaemon(true);
		monitorThread.setName("xxl-job, admin JobFailMonitorHelper");
		//启动线程
		monitorThread.start();
	}

The monitorThread daemon thread will check the log file of the call failure with a sleep cycle of 10 seconds, and then respond to the failure record.

6. Initialize and process the callback thread pool of the executor + monitor the daemon thread that the execution result of the task is lost

​ The initialization entry is: JobCompleteHelper.getInstance().start(), which initializes the thread pool for processing executor callbacks, initializes the threads that monitor the lost results of executor tasks, triggers the heartbeat mechanism (60 seconds by default), and the monitoring tasks have been scheduled successfully. However, the executor has not reported the processing status, the task status has been "running" (handle_code = 0), and the scheduling start time has passed 10 minutes, and the corresponding executor has no record of heartbeat registration (offline ), mark such records as execution failures. Look at the source code of the start method:

	public void start(){
    
    

		// for callback
		//创建处理执行器回调的线程池
		callbackThreadPool = new ThreadPoolExecutor(
				2,
				20,
				30L,
				TimeUnit.SECONDS,
				new LinkedBlockingQueue<Runnable>(3000),
				new ThreadFactory() {
    
    
					@Override
					public Thread newThread(Runnable r) {
    
    
						return new Thread(r, "xxl-job, admin JobLosedMonitorHelper-callbackThreadPool-" + r.hashCode());
					}
				},
				new RejectedExecutionHandler() {
    
    
					@Override
					public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    
    
						r.run();
						logger.warn(">>>>>>>>>>> xxl-job, callback too fast, match threadpool rejected handler(run now).");
					}
				});


		// for monitor
		//创建守护线程
		monitorThread = new Thread(new Runnable() {
    
    

			@Override
			public void run() {
    
    

				// wait for JobTriggerPoolHelper-init
				try {
    
    
					//休眠50毫秒,等待JobTriggerPoolHelper初始完成
					TimeUnit.MILLISECONDS.sleep(50);
				} catch (InterruptedException e) {
    
    
					if (!toStop) {
    
    
						logger.error(e.getMessage(), e);
					}
				}

				// monitor
				//不销毁一直监听
				while (!toStop) {
    
    
					try {
    
    
						// 任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本次调度主动标记失败;
						Date losedTime = DateUtil.addMinutes(new Date(), -10);
						//查询出已经调度成功,但是执行器一直没有反馈处理成功,任务状态一直是“运行中”(handle_code = 0),且调度开始时间到现在已经过去10分钟、且对应的执行器已经没有心跳注册的记录
						List<Long> losedJobIds  = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLostJobIds(losedTime);
                        //把这样的记录标记为失败
						if (losedJobIds!=null && losedJobIds.size()>0) {
    
    
							for (Long logId: losedJobIds) {
    
    

								XxlJobLog jobLog = new XxlJobLog();
								jobLog.setId(logId);

								jobLog.setHandleTime(new Date());
								jobLog.setHandleCode(ReturnT.FAIL_CODE);
								jobLog.setHandleMsg( I18nUtil.getString("joblog_lost_fail") );
								//完成此任务,并更新日志的状态值,有子任务再调用子任务
								XxlJobCompleter.updateHandleInfoAndFinish(jobLog);
							}

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

                    try {
    
    
                    	//休眠周期是60秒,心跳机制
                        TimeUnit.SECONDS.sleep(60);
                    } catch (Exception e) {
    
    
                        if (!toStop) {
    
    
                            logger.error(e.getMessage(), e);
                        }
                    }

                }

				logger.info(">>>>>>>>>>> xxl-job, JobLosedMonitorHelper stop");

			}
		});
		//设置为守护线程
		monitorThread.setDaemon(true);
		monitorThread.setName("xxl-job, admin JobLosedMonitorHelper");
		//启动线程
		monitorThread.start();
	}

The callbackThreadPool thread pool is mainly used for feedback processing to the scheduling center after the executor executes the task; the monitorThread daemon thread sequentially detects that the task status is running with a sleep cycle of 60 seconds, and the scheduling time has been greater than 10 minutes, and the executor has been If there is no record of registration (not online), mark it as execution failure, otherwise this log will always be running.

7. Initialize the daemon thread that calculates daily scheduling statistics and clears expired log records

​ The entry for this initialization is: JobLogReportHelper.getInstance().start(), which initializes the daemon thread for processing log reports, triggers the heartbeat mechanism (1 minute by default), pushes the current time forward for two days, and schedules within these three days The result report value, store the result value in the xxl_job_log_report table by day; if the configured maximum number of days to save the log is greater than 0, it will be cleaned up; if the current time minus the last cleaning time is greater than 1 day (milliseconds), the expired log record will be deleted . Look at the start source code:

   public void start(){
    
    
        //创建日志报表的守护线程
        logrThread = new Thread(new Runnable() {
    
    

            @Override
            public void run() {
    
    

                // last clean log time
                //上次清理日志时间
                long lastCleanLogTime = 0;

                //不销毁一直执行
                while (!toStop) {
    
    

                    // 1、log-report refresh: refresh log report in 3 days
                    try {
    
    
                        //处理当前时间往前推两天,这三天时间内的调度结果值
                        for (int i = 0; i < 3; i++) {
    
    

                            // today
                            Calendar itemDay = Calendar.getInstance();
                            itemDay.add(Calendar.DAY_OF_MONTH, -i);
                            itemDay.set(Calendar.HOUR_OF_DAY, 0);
                            itemDay.set(Calendar.MINUTE, 0);
                            itemDay.set(Calendar.SECOND, 0);
                            itemDay.set(Calendar.MILLISECOND, 0);
                            //当前时间减去i天的00:00:00时刻
                            Date todayFrom = itemDay.getTime();

                            itemDay.set(Calendar.HOUR_OF_DAY, 23);
                            itemDay.set(Calendar.MINUTE, 59);
                            itemDay.set(Calendar.SECOND, 59);
                            itemDay.set(Calendar.MILLISECOND, 999);
                            //当前时间减去i天的23:59:59时刻
                            Date todayTo = itemDay.getTime();

                            // refresh log-report every minute
                            XxlJobLogReport xxlJobLogReport = new XxlJobLogReport();
                            xxlJobLogReport.setTriggerDay(todayFrom);
                            xxlJobLogReport.setRunningCount(0);
                            xxlJobLogReport.setSucCount(0);
                            xxlJobLogReport.setFailCount(0);
                            //根据起止日期,从xxl_job_log日志表中查询这个时间段内总的执行次数、运行中次数、调度成功次数
                            Map<String, Object> triggerCountMap = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLogReport(todayFrom, todayTo);
                            if (triggerCountMap!=null && triggerCountMap.size()>0) {
    
    
                                //总的执行次数
                                int triggerDayCount = triggerCountMap.containsKey("triggerDayCount")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCount"))):0;
                                //运行中次数
                                int triggerDayCountRunning = triggerCountMap.containsKey("triggerDayCountRunning")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountRunning"))):0;
                                //调度成功次数
                                int triggerDayCountSuc = triggerCountMap.containsKey("triggerDayCountSuc")?Integer.valueOf(String.valueOf(triggerCountMap.get("triggerDayCountSuc"))):0;
                                //失败次数
                                int triggerDayCountFail = triggerDayCount - triggerDayCountRunning - triggerDayCountSuc;

                                xxlJobLogReport.setRunningCount(triggerDayCountRunning);
                                xxlJobLogReport.setSucCount(triggerDayCountSuc);
                                xxlJobLogReport.setFailCount(triggerDayCountFail);
                            }

                            // do refresh
                            //把某一天的调度日志报表记录更新进去
                            int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().update(xxlJobLogReport);
                            if (ret < 1) {
    
     //若是之前没有添加过,则进行插入
                                XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().save(xxlJobLogReport);
                            }
                        }

                    } catch (Exception e) {
    
    
                        if (!toStop) {
    
    
                            logger.error(">>>>>>>>>>> xxl-job, job log report thread error:{}", e);
                        }
                    }

                    // 2、log-clean: switch open & once each day
                    //配置的保存日志最大天数大于0则处理,当前时间减去上次清理时间大于1天(毫秒数)则进行日志的清除
                    if (XxlJobAdminConfig.getAdminConfig().getLogretentiondays()>0
                            && System.currentTimeMillis() - lastCleanLogTime > 24*60*60*1000) {
    
    

                        // expire-time
                        //清理日志的时间
                        Calendar expiredDay = Calendar.getInstance();
                        //当前时间减去配置的天数
                        expiredDay.add(Calendar.DAY_OF_MONTH, -1 * XxlJobAdminConfig.getAdminConfig().getLogretentiondays());
                        expiredDay.set(Calendar.HOUR_OF_DAY, 0);
                        expiredDay.set(Calendar.MINUTE, 0);
                        expiredDay.set(Calendar.SECOND, 0);
                        expiredDay.set(Calendar.MILLISECOND, 0);
                        //得到清理的时间
                        Date clearBeforeTime = expiredDay.getTime();

                        // clean expired log
                        //循环处理所有的大于最大存放日期的日志
                        List<Long> logIds = null;
                        do {
    
    
                            //查询调度日期在清理截止日期之前的日志记录
                            logIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findClearLogIds(0, 0, clearBeforeTime, 0, 1000);
                            if (logIds!=null && logIds.size()>0) {
    
    
                                //删除日志记录
                                XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().clearLog(logIds);
                            }
                        } while (logIds!=null && logIds.size()>0);

                        // update clean time
                        //重新设置上次清理时间
                        lastCleanLogTime = System.currentTimeMillis();
                    }

                    try {
    
    
                        //休眠1分钟,心跳注册机制
                        TimeUnit.MINUTES.sleep(1);
                    } catch (Exception e) {
    
    
                        if (!toStop) {
    
    
                            logger.error(e.getMessage(), e);
                        }
                    }

                }

                logger.info(">>>>>>>>>>> xxl-job, job log report thread stop");

            }
        });
        //设置为守护线程
        logrThread.setDaemon(true);
        logrThread.setName("xxl-job, admin JobLogReportHelper");
        //启动线程
        logrThread.start();
    }

The logrThread daemon thread counts the current time forward by 2 days with a 60-second sleep cycle, and counts the total number of successful, running, and failed executions of tasks in these 3 days, and stores the results in the xxl_job_log_report table for display on the web homepage , the screenshot of the web home page display is as follows:
insert image description here

The log record is also expired and cleaned. Although the cycle period of the daemon thread is 60 seconds, the log cleaning method will only be executed once a day. Each execution will reset the last cleaning time lastCleanLogTime, and each time the current time and the previous time will be judged. Check whether the cleanup time is greater than 1 day, and the log will be cleared only if the time is greater than 1 day. Configure the number of days to save the log in application.properties:

xxl.job.logretentiondays=30

If you don’t want to clear the log, you can configure the value to be less than 0, such as -1. The number of days configured here needs to be consistent with the number of days configured by the executor. Otherwise, there may be records in the xxl_job_log log table of the scheduling center, but they have been deleted in the executor directory. The execution log file corresponding to this task is deleted, so the details of the execution log cannot be accessed.

8. Initialize the daemon thread for reading ahead and executing tasks

​ The initialization entry is: JobScheduleHelper.getInstance().start(), which initializes the timing daemon thread and sleeps for 4 to 5 seconds each time. In order to prevent tasks from being repeatedly scheduled in a cluster environment, the database is used when pre-reading tasks Write locks are processed; pre-read tasks whose next execution time is within the current time + 5 seconds, and for tasks whose next execution time is more than 5 seconds behind the current time (not executed after expiration), the expiration scheduling policy is immediate Once executed, task scheduling is performed; tasks whose next execution time is within 5 seconds from the current time are scheduled; tasks whose next execution time is within +5 seconds from the current time are placed in the map, and the key of the map It is the number of seconds to execute the task, and value is the set of task ids that need to be executed in this second. Look at the source code of the start method:

  public void start(){
    
    

        // schedule thread
        scheduleThread = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    

                try {
    
    
                    //休眠5000 - System.currentTimeMillis()%1000毫秒,最大值的情况为5000-0,最小值的情况为5000-999
                    //随机休眠4到5秒的范围
                    TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 );
                } catch (InterruptedException e) {
    
    
                    if (!scheduleThreadToStop) {
    
    
                        logger.error(e.getMessage(), e);
                    }
                }
                logger.info(">>>>>>>>> init xxl-job admin scheduler success.");

                // pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20)
                //预读数量:按每个任务50ms计算,qps为20,快线程池+慢线程池最大线程数之和,再乘以20,即为1秒可以处理的最大任务量,默认是6000
                int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;

                while (!scheduleThreadToStop) {
    
    

                    // Scan Job
                    //起始时间
                    long start = System.currentTimeMillis();
                    //数据库连接
                    Connection conn = null;
                    //连接是否自动提交
                    Boolean connAutoCommit = null;
                    //预处理
                    PreparedStatement preparedStatement = null;

                    boolean preReadSuc = true;
                    try {
    
    
                        //获取数据库连接
                        conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection();
                        connAutoCommit = conn.getAutoCommit();
                        //关闭自动提交
                        conn.setAutoCommit(false);
                        //执行sql语句:对xxl_job_lock添加写锁,为了防止在集群环境中,任务被重复调度,所以使用写锁的方式处理
                        preparedStatement = conn.prepareStatement(  "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" );
                        preparedStatement.execute();

                        // tx start

                        // 1、pre read
                        long nowTime = System.currentTimeMillis();
                        //查询预执行的任务,且下次执行时间小于当前时间往后+5秒,最多查询可以处理的preReadCount数量
                        List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
                        if (scheduleList!=null && scheduleList.size()>0) {
    
    
                            // 2、push time-ring
                            for (XxlJobInfo jobInfo: scheduleList) {
    
    

                                // time-ring jump
                                //任务下次执行时间到当前时间已经差着5秒以上,说明已经过了调度时间了
                                if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
    
    
                                    // 2.1、trigger-expire > 5s:pass && make next-trigger-time
                                    logger.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = " + jobInfo.getId());

                                    // 1、misfire match
                                    //获取到此任务配置的过期调度策略
                                    MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING);
                                    //若是立即执行一次,则调用执行方法
                                    if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) {
    
    
                                        // FIRE_ONCE_NOW 》 trigger
                                        //调用任务执行方法,执行类型为调度过期补偿
                                        JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null);
                                        logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );
                                    }

                                    // 2、fresh next
                                    //重新设置任务的下次执行时间和上次执行时间
                                    refreshNextValidTime(jobInfo, new Date());

                                } else if (nowTime > jobInfo.getTriggerNextTime()) {
    
    
                                    //下次执行时间在当前时间减去5秒之内
                                    // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time

                                    // 1、trigger
                                    //调用任务执行方法,执行类型为Cron触发
                                    JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
                                    logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );

                                    // 2、fresh next
                                    //重新设置任务的下次执行时间和上次执行时间
                                    refreshNextValidTime(jobInfo, new Date());

                                    // next-trigger-time in 5s, pre-read again
                                    //经过上面重新设置了下次执行时间,新设置的下次执行时间还在当前时间加上5秒之内
                                    if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {
    
    

                                        // 1、make ring second
                                        //计算结果值范围:0到59之间
                                        int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                        // 2、push time ring
                                        //把任务id放到一个map集合中
                                        pushTimeRing(ringSecond, jobInfo.getId());

                                        // 3、fresh next
                                        //重新设置任务的下次执行时间和上次执行时间
                                        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                    }

                                } else {
    
    
                                    //下次执行时间在当前时间往后延5秒之内
                                    // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time

                                    // 1、make ring second
                                    //计算结果值范围:0到59之间
                                    int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                    // 2、push time ring
                                    //把任务id放到一个map集合中
                                    pushTimeRing(ringSecond, jobInfo.getId());

                                    // 3、fresh next
                                    //重新设置任务的下次执行时间和上次执行时间
                                    refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                }

                            }

                            // 3、update trigger info
                            //更新任务下次执行时间和上次执行时间
                            for (XxlJobInfo jobInfo: scheduleList) {
    
    
                                XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo);
                            }

                        } else {
    
    
                            //没有进行预读处理
                            preReadSuc = false;
                        }

                        // tx stop


                    } catch (Exception e) {
    
    
                        if (!scheduleThreadToStop) {
    
    
                            logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e);
                        }
                    } finally {
    
    

                        // commit
                        if (conn != null) {
    
    
                            try {
    
    
                                //提交数据库,释放写锁
                                conn.commit();
                            } catch (SQLException e) {
    
    
                                if (!scheduleThreadToStop) {
    
    
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            try {
    
    
                                //还原数据库连接的自动提交设置
                                conn.setAutoCommit(connAutoCommit);
                            } catch (SQLException e) {
    
    
                                if (!scheduleThreadToStop) {
    
    
                                    logger.error(e.getMessage(), e);
                                }
                            }
                            try {
    
    
                                //关闭连接
                                conn.close();
                            } catch (SQLException e) {
    
    
                                if (!scheduleThreadToStop) {
    
    
                                    logger.error(e.getMessage(), e);
                                }
                            }
                        }

                        // close PreparedStatement
                        //关闭预处理
                        if (null != preparedStatement) {
    
    
                            try {
    
    
                                preparedStatement.close();
                            } catch (SQLException e) {
    
    
                                if (!scheduleThreadToStop) {
    
    
                                    logger.error(e.getMessage(), e);
                                }
                            }
                        }
                    }
                    //计算花费的时间
                    long cost = System.currentTimeMillis()-start;


                    // Wait seconds, align second
                    //花费时间小于1秒则让程序休眠,大于1秒则不休眠
                    if (cost < 1000) {
    
      // scan-overtime, not wait
                        try {
    
    
                            // pre-read period: success > scan each second; fail > skip this period;
                            //线程休眠,休眠时间计算:若是有预读,则使用1000减去0到999(0到1秒之间);没有预读,则使用5000减去0到999(4到5秒之间)
                            //查询任务的定时周期为5秒,若是没有预读,则休眠时间在(0到1之间)+默认的(4到5秒)之间;有预读,则预读处理的时候已经把任务下次执行时间在当前时间+5秒之内任务加到环map中了,所以这里再休眠4到5秒,要不然就是白跑一趟
                            TimeUnit.MILLISECONDS.sleep((preReadSuc?1000:PRE_READ_MS) - System.currentTimeMillis()%1000);
                        } catch (InterruptedException e) {
    
    
                            if (!scheduleThreadToStop) {
    
    
                                logger.error(e.getMessage(), e);
                            }
                        }
                    }

                }

                logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread stop");
            }
        });
        //设置为守护线程
        scheduleThread.setDaemon(true);
        scheduleThread.setName("xxl-job, admin JobScheduleHelper#scheduleThread");
        //启动线程
        scheduleThread.start();
}

The scheduleThread daemon thread takes 4 to 5 seconds as the sleep cycle to load the tasks whose next execution time is within the current time + 5 seconds. In order to prevent tasks from being repeatedly loaded and scheduled in a cluster environment, the database write lock is used when pre-reading tasks The way of processing, the executed sql statement:

select * from xxl_job_lock where lock_name = 'schedule_lock' for update

Calculate the amount that can be pre-read according to the size of the fast and slow thread pool, and query the record that the next execution time of the task is less than the current time plus 5 seconds; for the task that has a difference of more than 5 seconds between the next execution time and the current time (not executed after the expiration date) , the expired scheduling strategy is to execute once immediately, then schedule the task and reset the next execution time; if the next execution time is within 5 seconds from the current time, call the task execution method and reset the next execution time , the newly set next execution time is still within 5 seconds from the current time, then add this task to the ring map; the next execution time is delayed within 5 seconds from the current time (before the execution time), Then add this task to the map and reset the next execution time. If the time spent above is less than 1 second, let the program sleep, if it takes more than 1 second, it will not sleep; sleep time calculation: if there is pre-reading, use 1000 minus 0 to 999 (between 0 and 1 second); if there is no pre-reading, use 5000 minus 0 to 999 (between 4 and 5 seconds); the timing period of the query task is 5 seconds, if there is pre-reading, the sleep time is between (0 and 1) + default (4 to 5 seconds) If there is no pre-reading, it means that there is no task to be executed in the next 5 seconds. Here, it sleeps for 4 to 5 seconds. During the pre-reading process, the next execution time of the task has been added to the task within the current time + 5 seconds. It's in the map.

9. Initialize the daemon thread that processes the read-ahead task

​ The initialization entry is: JobScheduleHelper.getInstance().start(), which initializes the daemon thread that processes the pre-reading tasks. Each sleep cycle is between 0 and 1 second, and executes the pre-execution tasks placed in the map. According to the current second The number is the key, and the task is taken out from the map for scheduling. Look at the source code of the start method:

 public void start(){
    
    

        // ring thread
        //创建环形线程,用于处理上面定时线程预读任务(周期5秒左右)的时候,对于下次执行时间在当前时间+5秒内的任务,使用此线程来进行调度
        ringThread = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    

                while (!ringThreadToStop) {
    
    

                    // align second
                    try {
    
    
                        //随机休眠1到1000毫秒,在1秒范围内
                        TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
                    } catch (InterruptedException e) {
    
    
                        if (!ringThreadToStop) {
    
    
                            logger.error(e.getMessage(), e);
                        }
                    }

                    try {
    
    
                        // second data
                        List<Integer> ringItemData = new ArrayList<>();
                        //获取当前的秒数
                        int nowSecond = Calendar.getInstance().get(Calendar.SECOND);   // 避免处理耗时太长,跨过刻度,向前校验一个刻度;
                        for (int i = 0; i < 2; i++) {
    
    
                            //ringData存放的是预处理时,当前时间+5秒内需要执行的任务,使用此map对象存放的集合在ringThread线程中进行处理
                            //ringData的key是5秒内预处理的任务的秒数
                            List<Integer> tmpData = ringData.remove( (nowSecond+60-i)%60 );
                            if (tmpData != null) {
    
    
                                ringItemData.addAll(tmpData);
                            }
                        }

                        // ring trigger
                        logger.debug(">>>>>>>>>>> xxl-job, time-ring beat : " + nowSecond + " = " + Arrays.asList(ringItemData) );
                        //当前这一秒下有任务要处理
                        if (ringItemData.size() > 0) {
    
    
                            // do trigger
                            //循环处理这一秒下的任务
                            for (int jobId: ringItemData) {
    
    
                                // do trigger
                                //调用任务执行方法,执行类型为Cron触发
                                JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);
                            }
                            // clear
                            ringItemData.clear();
                        }
                    } catch (Exception e) {
    
    
                        if (!ringThreadToStop) {
    
    
                            logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
            }
        });
        //设置为守护线程
        ringThread.setDaemon(true);
        ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
        //启动线程
        ringThread.start();
    }

The ringThread daemon thread loops the pre-reading (within 5 seconds) tasks added to the map with a sleep cycle between 0 and 1 second, and takes out the tasks that need to be executed in this second from the map according to the current number of seconds as the key for scheduling .

10. Initial resource summary diagram

(1) Initialized thread pool
insert image description here

(2) Initialized daemon thread
insert image description here

2. Proactively initiate a request

​ The request initiated by the dispatch center to the executor can be found from the ExecutorBizClient class of the dispatch center client, which exists in the public core xxl-job-core project of xxl-job, and the directory structure is com.xxl.job.core.biz. client, from the class, you can see five methods including beat, idleBeat, run, kill, and log. Look at the source code of ExecutorBizClient:

/**
 * 调度中心-》调用执行器的客户端,供调度中心使用
 */
public class ExecutorBizClient implements ExecutorBiz {
    
    

    public ExecutorBizClient() {
    
    
    }
    public ExecutorBizClient(String addressUrl, String accessToken) {
    
    
        this.addressUrl = addressUrl;
        this.accessToken = accessToken;

        // valid
        if (!this.addressUrl.endsWith("/")) {
    
    
            this.addressUrl = this.addressUrl + "/";
        }
    }

    private String addressUrl ;
    private String accessToken;
    private int timeout = 3;

    //心跳检测执行器是否在线,用于故障转移方式时的调用测试
    @Override
    public ReturnT<String> beat() {
    
    
        //发起http远程调用
        return XxlJobRemotingUtil.postBody(addressUrl+"beat", accessToken, timeout, "", String.class);
    }

    //心跳检测执行器是否忙碌,用于忙碌转移方式时的调用测试
    @Override
    public ReturnT<String> idleBeat(IdleBeatParam idleBeatParam){
    
    
         //发起http远程调用
        return XxlJobRemotingUtil.postBody(addressUrl+"idleBeat", accessToken, timeout, idleBeatParam, String.class);
    }

    //调用执行器运行
    @Override
    public ReturnT<String> run(TriggerParam triggerParam) {
    
    
         //发起http远程调用
        return XxlJobRemotingUtil.postBody(addressUrl + "run", accessToken, timeout, triggerParam, String.class);
    }

    //停止执行器的执行
    @Override
    public ReturnT<String> kill(KillParam killParam) {
    
    
         //发起http远程调用
        return XxlJobRemotingUtil.postBody(addressUrl + "kill", accessToken, timeout, killParam, String.class);
    }

    //查询执行器产生的执行日志信息
    @Override
    public ReturnT<LogResult> log(LogParam logParam) {
    
    
         //发起http远程调用
        return XxlJobRemotingUtil.postBody(addressUrl + "log", accessToken, timeout, logParam, LogResult.class);
    }
}

This client class provides 5 methods for calling the executor. When the method is called, the interface address of the remote executor will be spliced ​​to make a remote call. When you need to initiate a request to an executor, you need to create a client for this executor, and store this client in the map. The key is the address of the executor, and the value is the client. When you need to send this executor next time When initiating a request, just get the client directly from the map. When creating a client, you need to pass in the actuator address and token as parameters, so that you can directly splice the url and pass the token when initiating a remote call. Take a look at the source code for obtaining the client based on the address of the executor:

ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address)

This method calls to the XxlJobScheduler class and uses the ExecutorBiz parent interface to receive clients. Look at the source code of the getExecutorBiz method:

    //使用集合记录执行器地址和它的客户端,key:执行器地址,value:调用执行器的客户端
    private static ConcurrentMap<String, ExecutorBiz> executorBizRepository = new ConcurrentHashMap<String, ExecutorBiz>();

    public static ExecutorBiz getExecutorBiz(String address) throws Exception {
    
    
        // valid
        if (address==null || address.trim().length()==0) {
    
    
            return null;
        }

        // load-cache
        address = address.trim();
        //已经创建过则直接使用
        ExecutorBiz executorBiz = executorBizRepository.get(address);
        if (executorBiz != null) {
    
    
            return executorBiz;
        }

        // set-cache
        //没有创建过则创建
        executorBiz = new ExecutorBizClient(address, XxlJobAdminConfig.getAdminConfig().getAccessToken());
        //放到map集合中供下次使用
        executorBizRepository.put(address, executorBiz);
        return executorBiz;
    }

1. beat call

​ The beat call is for the dispatch center to detect whether the executor is online, and it is used for the call test in the failover mode. The call location in the source code:

public class ExecutorRouteFailover extends ExecutorRouter {
    
    

    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
    
    

        StringBuffer beatResultSB = new StringBuffer();
        //遍历执行器地址
        for (String address : addressList) {
    
    
            // beat
            ReturnT<String> beatResult = null;
            try {
    
    
                //根据调用地址获取它对应的执行器客户端
                ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
                //调用beat方法,发送请求
                beatResult = executorBiz.beat();
            } catch (Exception e) {
    
    
                logger.error(e.getMessage(), e);
                beatResult = new ReturnT<String>(ReturnT.FAIL_CODE, ""+e );
            }
            beatResultSB.append( (beatResultSB.length()>0)?"<br><br>":"")
                    .append(I18nUtil.getString("jobconf_beat") + ":")
                    .append("<br>address:").append(address)
                    .append("<br>code:").append(beatResult.getCode())
                    .append("<br>msg:").append(beatResult.getMsg());

            // beat success
            //执行器还在线,则使用此执行器地址进行调用
            if (beatResult.getCode() == ReturnT.SUCCESS_CODE) {
    
    

                beatResult.setMsg(beatResultSB.toString());
                //能调通的执行器地址
                beatResult.setContent(address);
                return beatResult;
            }
        }
        return new ReturnT<String>(ReturnT.FAIL_CODE, beatResultSB.toString());

    }
}

When the routing strategy is failover, in the executor set, select the executor that can be tuned and has no faults to call the task. To determine whether a certain actuator is faulty, you only need to call its beat interface. If there is feedback, there is no fault, and if there is no feedback, there is fault.

2. idleBeat call

​ The idleBeat call is to detect whether the executor is busy. It is used for the call test in the busy transfer mode. The calling position in the source code:

public class ExecutorRouteBusyover extends ExecutorRouter {
    
    

    @Override
    public ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {
    
    
        StringBuffer idleBeatResultSB = new StringBuffer();
        //遍历所有执行器
        for (String address : addressList) {
    
    
            // beat
            ReturnT<String> idleBeatResult = null;
            try {
    
    
                //根据地址获取调用执行器的客户端
                ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
                //执行调用
                idleBeatResult = executorBiz.idleBeat(new IdleBeatParam(triggerParam.getJobId()));
            } catch (Exception e) {
    
    
                logger.error(e.getMessage(), e);
                idleBeatResult = new ReturnT<String>(ReturnT.FAIL_CODE, ""+e );
            }
            idleBeatResultSB.append( (idleBeatResultSB.length()>0)?"<br><br>":"")
                    .append(I18nUtil.getString("jobconf_idleBeat") + ":")
                    .append("<br>address:").append(address)
                    .append("<br>code:").append(idleBeatResult.getCode())
                    .append("<br>msg:").append(idleBeatResult.getMsg());

            // beat success
            //调用成功,表示此执行器当前不处于忙碌状态
            if (idleBeatResult.getCode() == ReturnT.SUCCESS_CODE) {
    
    
                idleBeatResult.setMsg(idleBeatResultSB.toString());
                idleBeatResult.setContent(address);
                return idleBeatResult;
            }
        }

        return new ReturnT<String>(ReturnT.FAIL_CODE, idleBeatResultSB.toString());
    }
}

When the routing policy is busy transfer, in the set of executors, select the executor that is not currently processing the task to execute the task, if the current executor is executing the task (the last scheduling of the task, the executor has not completed yet, come again Scheduling), it returns failure until an executor that does not process this task is found to call. Note: Busy transfer refers to whether the task executor is still executing, not whether the executor is executing the task. For example, the executor is currently executing other tasks, and it is not considered busy.

3. run call

​ The run call refers to calling the executor to execute the task. xxl-job provides 6 types of scheduling methods. See the source code of the scheduling method enumeration class TriggerTypeEnum:

//调度方式枚举类
public enum TriggerTypeEnum {
    
    
    //手动触发
    MANUAL(I18nUtil.getString("jobconf_trigger_type_manual")),
    //Cron触发
    CRON(I18nUtil.getString("jobconf_trigger_type_cron")),
    //失败重试触发
    RETRY(I18nUtil.getString("jobconf_trigger_type_retry")),
    //父任务触发
    PARENT(I18nUtil.getString("jobconf_trigger_type_parent")),
    //API触发
    API(I18nUtil.getString("jobconf_trigger_type_api")),
    //调度过期补偿
    MISFIRE(I18nUtil.getString("jobconf_trigger_type_misfire"));

    private TriggerTypeEnum(String title){
    
    
        this.title = title;
    }
    private String title;
    public String getTitle() {
    
    
        return title;
    }
}

MANUAL: Manually trigger a schedule, which is triggered when the web page clicks the execute button;

CRON: Automatically trigger task scheduling when the execution time is up, which is used to trigger when the pre-reading daemon thread loads the pre-reading task and the daemon thread that processes the pre-reading task arrives at the time;

RETRY: After task scheduling failure or execution failure, it is triggered by the daemon thread monitoring task scheduling failure or execution failure;

PARENT: Triggered by the parent task, when the dispatch center receives the feedback of the successful execution of the executor, the loss record of the processing task result, and the feedback of stopping the execution of the executor, after updating the execution result, if the task has configuration subtasks, Then trigger the execution of subtasks;

API: API trigger, this type is not used in xxl-job at present;

MISFIRE: Scheduling overdue compensation, used when the read-ahead daemon thread loads tasks whose next execution time is within the current time + 5 seconds, and triggers once for tasks whose next execution time is before the current time - 5 seconds (not executed after timeout) Compensation schedule.

​ For the call in the source code, we start from manually triggering the execution once. When clicking to execute a certain task in the web interface, the screenshot of the page is as follows:
insert image description here

The control class called is JobInfoController, see the source code of the interface method:

	//触发调度
	@RequestMapping("/trigger")
	@ResponseBody
	public ReturnT<String> triggerJob(int id, String executorParam, String addressList) {
    
    
		if (executorParam == null) {
    
    
			executorParam = "";
		}
        //调度类型为手动触发
		JobTriggerPoolHelper.trigger(id, TriggerTypeEnum.MANUAL, -1, null, executorParam, addressList);
		return ReturnT.SUCCESS;
	}

The class that handles task scheduling is JobTriggerPoolHelper, look at its trigger source code:

   private static JobTriggerPoolHelper helper = new JobTriggerPoolHelper();

    /**
     * 执行任务调度
     * @param jobId 任务id
     * @param triggerType 调度类型
     * @param failRetryCount 失败重试次数,大于等于0才生效
     * 			>=0: use this param
     * 			<0: use param from job info config
     * @param executorShardingParam    //执行器分片信息
     * @param executorParam   //任务参数
     * @param addressList     //执行器地址
     */
    public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) {
    
    
        helper.addTrigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
    }

The JobTriggerPoolHelper class first creates its own object, and then calls addTrigger to add a scheduling task method. See the source code of addTrigger:

   //使用volatile修饰变量是为了在多线程下,每个线程能及时拿到最新的minTim值
   private volatile long minTim = System.currentTimeMillis()/60000;     // ms > min 计算出来的是分钟
    //jobTimeoutCountMap是以一分钟为口径进行统计的,一分钟内某个任务调度超过500毫秒的次数,根据此次数来选择使用快(小于10次)、慢(大于10次)线程池执行此任务
    private volatile ConcurrentMap<Integer, AtomicInteger> jobTimeoutCountMap = new ConcurrentHashMap<>();

   public void addTrigger(final int jobId,
                           final TriggerTypeEnum triggerType,
                           final int failRetryCount,
                           final String executorShardingParam,
                           final String executorParam,
                           final String addressList) {
    
    

        // choose thread pool
        //选择使用快的还是慢的线程池执行此任务
        ThreadPoolExecutor triggerPool_ = fastTriggerPool;
        //使用jobTimeoutCountMap来存放任务超时次数集合,key:任务id,value:次数,它统计的口径是一分钟内的调度数据
        AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
        //在1分钟内,此任务有10次以上超过500毫米才调度完成,使用慢线程池处理
        if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) {
    
          // job-timeout 10 times in 1 min
            //使用慢线程池处理
            triggerPool_ = slowTriggerPool;
        }

        // trigger
        //线程池执行任务
        triggerPool_.execute(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                //调度开始时间
                long start = System.currentTimeMillis();

                try {
    
    
                    // do trigger
                    //开始进行调度
                    XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
                } catch (Exception e) {
    
    
                    logger.error(e.getMessage(), e);
                } finally {
    
    

                    // check timeout-count-map
                    //调度结束后的时间-分钟
                    long minTim_now = System.currentTimeMillis()/60000;
                    //调度结束后的分钟数不等于设置的分钟数
                    if (minTim != minTim_now) {
    
    
                        //重新设置分钟数,作为下一个统计口径的时间点
                        minTim = minTim_now;
                        //清空map集合,已经过了minTim这一分钟的统计口径,jobTimeoutCountMap是以一分钟为口径进行统计的
                        jobTimeoutCountMap.clear();
                    }

                    // incr timeout-count-map
                    //计算一共花费了多少时间
                    long cost = System.currentTimeMillis()-start;
                    //调度时间大于500毫秒
                    if (cost > 500) {
    
           // ob-timeout threshold 500ms
                        //putIfAbsent:向map中添加记录,若是存在此key的记录,则返回value,若是不存在则插入,插入的时候返回的值为null
                        AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1));
                        //已经存在此key的值,timeoutCount才不等于null
                        if (timeoutCount != null) {
    
    
                            //使用AtomicInteger线程安全的方式把次数加1(cas自旋的方式,先比对在加1)
                            timeoutCount.incrementAndGet();
                        }
                    }

                }

            }
        });
    }

There are two thread pools, fast and slow, to process this task. By default, the fast thread pool is selected for execution, and the conditions for selecting the slow thread pool: within 1 minute, this task has more than 10 times of more than 500 mm before scheduling is completed, and the slow thread pool is used for processing . Here volatile is used to modify minTim, so that each thread can get the latest minTim value in time under multi-threading; use jobTimeoutCountMap to store the number of times a task is scheduled for more than 500 milliseconds within one minute, which is also modified by volatile. When the task execution is completed, judge whether the current minute is still equal to minTim. If it is equal, it means that it is still within the minute of minTim; if it is not equal to (greater than), it means that the statistical dimension of the minute of minTim has passed, and the jobTimeoutCountMap needs to be cleared, and Assign the current minute to minTim; the scheduling time is greater than 500 milliseconds, use the putIfAbsent method to add a record to the map, if there is a record for this key, return the value, do not insert, if it does not exist, insert, the value returned when inserting It is null (both the put method and the putIfAbsent method will return the old value, the difference is that when the key exists, the put method will overwrite it, but putIfAbsent will not overwrite it). If the value of this key already exists, add 1 to the original AtomicInteger value in a thread-safe way, and if it does not exist, the inserted AtomicInteger value will be 1.

​ Finally call XxlJobTrigger.trigger() to execute the scheduling method, see the source code of the trigger() method:

    public static void trigger(int jobId,
                               TriggerTypeEnum triggerType,
                               int failRetryCount,
                               String executorShardingParam,
                               String executorParam,
                               String addressList) {
    
    

        // load data
        //加载任务数据
        XxlJobInfo jobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(jobId);
        if (jobInfo == null) {
    
    
            logger.warn(">>>>>>>>>>>> trigger fail, jobId invalid,jobId={}", jobId);
            return;
        }
        //有新设置的执行参数,则进行参数的覆盖
        if (executorParam != null) {
    
    
            jobInfo.setExecutorParam(executorParam);
        }
        //调度失败重试次数
        int finalFailRetryCount = failRetryCount>=0?failRetryCount:jobInfo.getExecutorFailRetryCount();
        //执行器分组信息
        XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(jobInfo.getJobGroup());

        // cover addressList
        //有设置新的执行器地址,则进行覆盖
        if (addressList!=null && addressList.trim().length()>0) {
    
    
            group.setAddressType(1);
            group.setAddressList(addressList.trim());
        }

        // sharding param
        //分片参数
        int[] shardingParam = null;
        if (executorShardingParam!=null){
    
    
            String[] shardingArr = executorShardingParam.split("/");
            if (shardingArr.length==2 && isNumeric(shardingArr[0]) && isNumeric(shardingArr[1])) {
    
    
                shardingParam = new int[2];
                shardingParam[0] = Integer.valueOf(shardingArr[0]);
                shardingParam[1] = Integer.valueOf(shardingArr[1]);
            }
        }
        //路由策略是分片广播
        if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null)
                && group.getRegistryList()!=null && !group.getRegistryList().isEmpty()
                && shardingParam==null) {
    
    
            //分片广播,则需要向所有的注册器都进行调用
            for (int i = 0; i < group.getRegistryList().size(); i++) {
    
    
                //处理调度
                processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size());
            }
        } else {
    
    //非分片广播
            if (shardingParam == null) {
    
    
                shardingParam = new int[]{
    
    0, 1};
            }
            //处理调度
            processTrigger(group, jobInfo, finalFailRetryCount, triggerType, shardingParam[0], shardingParam[1]);
        }
    }

trigger方法只是做一下调用的前置处理,根据任务id查询出任务,若是有新的执行参数,则覆盖xxl_job_info表中配置的参数;查询出执行器分组信息,获取到有哪些执行器可以调用;参数涉及到分片信息,所以默认创建只有一个分片的数组,索引为0;若是此任务配置的路由策略是分片广播,则所有的执行器都要执行任务,根据有多少个执行器来确定分为多少片,调用执行器的时候,传递当前执行器是第几个分片,总共有多少个分片(执行器在处理任务对应的具体方法时,处理的逻辑为:先查询这次任务涉及到的总记录数,需要按某个字段进行排序,然后用总记录数除以总的执行器数,得到每个执行器处理的平均执行数,最后一个执行器的执行数量为总记录数-平均执行数乘以(执行器数量-1),然后可以根据当前执行器所属的分片数,来查询到此执行器需要处理的记录范围,例如mysql的limit start,end语句,start为分片索引*平均执行数,end为执行数量)。广播分片执行器处理举例说明:

        //获取当前分片序号
        int shardIndex = XxlJobHelper.getShardIndex();
        //获取总分片数
        int shardTotal = XxlJobHelper.getShardTotal();
        //总记录条数--查询数据库
        int targetTotal = xxService.getTargetTotal();
        //查询记录的起始位置
        int start = shardIndex;
        //查询记录的offset
        int end = 1;
        //总记录大于分片数量
        if(targetTotal > shardTotal){
    
    
            //计算每个分片平均处理的数量
            int avgTotal = targetTotal/shardTotal;
            //数据查询起始
            start = shardIndex*avgTotal;
            //数据的offset
            end = avgTotal;
            //最后一个执行器
            if(shardIndex == shardTotal-1) {
    
    
                //总数量-前面几个执行器执行的数量
                end = targetTotal-(avgTotal*(shardTotal-1);
            } 
        }
        //使用start和end去查询需要处理的数据
        //拼接mysql语句:limit start end
                                   
        //对查询到的记录进行处理...

​ 任务的处理调用processTrigger方法,看下processTrigger源码:

    private static void processTrigger(XxlJobGroup group, XxlJobInfo jobInfo, int finalFailRetryCount, TriggerTypeEnum triggerType, int index, int total){
    
    

        // param
        //阻塞处理策略
        ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), ExecutorBlockStrategyEnum.SERIAL_EXECUTION);  // block strategy
        //路由策略
        ExecutorRouteStrategyEnum executorRouteStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null);    // route strategy
        //当路由策略为分片广播,组织分片参数
        String shardingParam = (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==executorRouteStrategyEnum)?String.valueOf(index).concat("/").concat(String.valueOf(total)):null;

        // 1、save log-id
        //新建一条日志信息,添加上执行时间
        XxlJobLog jobLog = new XxlJobLog();
        jobLog.setJobGroup(jobInfo.getJobGroup());
        jobLog.setJobId(jobInfo.getId());
        jobLog.setTriggerTime(new Date());
        XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().save(jobLog);
        logger.debug(">>>>>>>>>>> xxl-job trigger start, jobId:{}", jobLog.getId());

        // 2、init trigger-param
        //构造调度任务的参数
        TriggerParam triggerParam = new TriggerParam();
        triggerParam.setJobId(jobInfo.getId());
        triggerParam.setExecutorHandler(jobInfo.getExecutorHandler());
        triggerParam.setExecutorParams(jobInfo.getExecutorParam());
        triggerParam.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy());
        triggerParam.setExecutorTimeout(jobInfo.getExecutorTimeout());
        triggerParam.setLogId(jobLog.getId());
        triggerParam.setLogDateTime(jobLog.getTriggerTime().getTime());
        triggerParam.setGlueType(jobInfo.getGlueType());
        triggerParam.setGlueSource(jobInfo.getGlueSource());
        triggerParam.setGlueUpdatetime(jobInfo.getGlueUpdatetime().getTime());
        triggerParam.setBroadcastIndex(index);
        triggerParam.setBroadcastTotal(total);

        // 3、init address
        //获取到执行器的地址
        String address = null;
        ReturnT<String> routeAddressResult = null;
        if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) {
    
    
            if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) {
    
    
                //路由为分片广播的方式,根据分片的索引index获取到执行器的地址
                if (index < group.getRegistryList().size()) {
    
    
                    address = group.getRegistryList().get(index);
                } else {
    
    
                    address = group.getRegistryList().get(0);
                }
            } else {
    
    
                //根据配置的路由策略,从注册执行器列表,匹配出此次调度的执行器地址,路由策略包含随机、故障转移、忙碌转移等
                routeAddressResult = executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList());
                if (routeAddressResult.getCode() == ReturnT.SUCCESS_CODE) {
    
    
                    address = routeAddressResult.getContent();
                }
            }
        } else {
    
    //执行器地址为空,异常
            routeAddressResult = new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("jobconf_trigger_address_empty"));
        }

        // 4、trigger remote executor
        ReturnT<String> triggerResult = null;
        //执行器地址不为空,在进行调度
        if (address != null) {
    
    
            //执行调度
            triggerResult = runExecutor(triggerParam, address);
        } else {
    
    
            triggerResult = new ReturnT<String>(ReturnT.FAIL_CODE, null);
        }

        // 5、collection trigger info
        //构造调度执行器的调度-日志信息
        StringBuffer triggerMsgSb = new StringBuffer();
        triggerMsgSb.append(I18nUtil.getString("jobconf_trigger_type")).append(":").append(triggerType.getTitle());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_admin_adress")).append(":").append(IpUtil.getIp());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regtype")).append(":")
                .append( (group.getAddressType() == 0)?I18nUtil.getString("jobgroup_field_addressType_0"):I18nUtil.getString("jobgroup_field_addressType_1") );
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobconf_trigger_exe_regaddress")).append(":").append(group.getRegistryList());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorRouteStrategy")).append(":").append(executorRouteStrategyEnum.getTitle());
        if (shardingParam != null) {
    
    
            triggerMsgSb.append("("+shardingParam+")");
        }
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorBlockStrategy")).append(":").append(blockStrategy.getTitle());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_timeout")).append(":").append(jobInfo.getExecutorTimeout());
        triggerMsgSb.append("<br>").append(I18nUtil.getString("jobinfo_field_executorFailRetryCount")).append(":").append(finalFailRetryCount);

        triggerMsgSb.append("<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_run") +"<<<<<<<<<<< </span><br>")
                .append((routeAddressResult!=null&&routeAddressResult.getMsg()!=null)?routeAddressResult.getMsg()+"<br><br>":"").append(triggerResult.getMsg()!=null?triggerResult.getMsg():"");

        // 6、save log trigger-info
        //设置日志其他相关字段,供调度失败再次调度时候使用
        jobLog.setExecutorAddress(address);
        jobLog.setExecutorHandler(jobInfo.getExecutorHandler());
        jobLog.setExecutorParam(jobInfo.getExecutorParam());
        jobLog.setExecutorShardingParam(shardingParam);
        jobLog.setExecutorFailRetryCount(finalFailRetryCount);
        //jobLog.setTriggerTime();
        //设置调度结果状态值
        jobLog.setTriggerCode(triggerResult.getCode());
        //设置调度信息
        jobLog.setTriggerMsg(triggerMsgSb.toString());
        //更新日志记录
        XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(jobLog);

        logger.debug(">>>>>>>>>>> xxl-job trigger end, jobId:{}", jobLog.getId());
    }

processTrigger方法新建了一条执行日志记录,插入到xxl_job_log表中;构造调度任务的参数实体TriggerParam,作为调用执行器的传递参数封装;获取执行此次任务的执行器地址,若是路由策略为分片广播的方式,根据分片的索引index从执行器集合中获取执行器地址,若是其他路由方式,则使用对应的策略获取到执行器地址,例如随机、故障转移、忙碌转移等,故障转移就是上面介绍的beat调用,忙碌转移就是上面介绍的idleBeat调用;然后拿着构造好的参数类TriggerParam、匹配到的执行器地址address,执行任务的调用runExecutor;等到执行器返回调用结果后,构造调度执行器的详细日志信息,把调用的结果状态值和日志信息更新到一开始插入的日志表中,此日志记录着调用需要的所有参数,这样在进行失败重调的时候参数直接从日志记录中取。注意这里执行器只是返回是否调度成功,不返回具体是否执行成功,执行情况是等待执行器主动调用调度中心进行反馈。

​ 任务的具体调度是runExecutor方法,看下runExecutor源码:

   public static ReturnT<String> runExecutor(TriggerParam triggerParam, String address){
    
    
        ReturnT<String> runResult = null;
        try {
    
    
            //根据执行器地址获取调度执行器的客户端
            ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
            //执行调度方法
            runResult = executorBiz.run(triggerParam);
        } catch (Exception e) {
    
    
            logger.error(">>>>>>>>>>> xxl-job trigger error, please check if the executor[{}] is running.", address, e);
            runResult = new ReturnT<String>(ReturnT.FAIL_CODE, ThrowableUtil.toString(e));
        }

        StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":");
        runResultSB.append("<br>address:").append(address);
        runResultSB.append("<br>code:").append(runResult.getCode());
        runResultSB.append("<br>msg:").append(runResult.getMsg());

        runResult.setMsg(runResultSB.toString());
        return runResult;
    }

runExecutor方法根据执行器地址获取到调用客户端,然后执行此客户端的run方法,即调用到ExecutorBizClient类的run方法,此run方法发起远程http调用,到此一次完整的调用流程走完。run方法的源码:

    //调用执行器运行
    @Override
    public ReturnT<String> run(TriggerParam triggerParam) {
    
    
        //发起http远程调用
        return XxlJobRemotingUtil.postBody(addressUrl + "run", accessToken, timeout, triggerParam, String.class);
    }

4.kill调用

​ kill调用是调度中心对执行器正在处理的任务进行停止处理,当任务调度成功后,还没有收到执行器的反馈,调度中心可以调用kill来停止执行器的执行。在web 中的操作界面如下:

insert image description here

点击终止任务对应的接口为/joblog/logKill,看下logKill接口的源码:

	@RequestMapping("/logKill")
	@ResponseBody
	public ReturnT<String> logKill(int id){
    
    
		// base check
		XxlJobLog log = xxlJobLogDao.load(id);
		XxlJobInfo jobInfo = xxlJobInfoDao.loadById(log.getJobId());
		//任务不存在
		if (jobInfo==null) {
    
    
			return new ReturnT<String>(500, I18nUtil.getString("jobinfo_glue_jobid_unvalid"));
		}
		//调用执行器没有成功
		if (ReturnT.SUCCESS_CODE != log.getTriggerCode()) {
    
    
			return new ReturnT<String>(500, I18nUtil.getString("joblog_kill_log_limit"));
		}

		// request of kill
		ReturnT<String> runResult = null;
		try {
    
    
			//根据执行器地址获取对应的客户端
			ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(log.getExecutorAddress());
			//调用方法
			runResult = executorBiz.kill(new KillParam(jobInfo.getId()));
		} catch (Exception e) {
    
    
			logger.error(e.getMessage(), e);
			runResult = new ReturnT<String>(500, e.getMessage());
		}
        //停止任务执行成功
		if (ReturnT.SUCCESS_CODE == runResult.getCode()) {
    
    
			//把执行器处理状态设置为失败
			log.setHandleCode(ReturnT.FAIL_CODE);
			log.setHandleMsg( I18nUtil.getString("joblog_kill_log_byman")+":" + (runResult.getMsg()!=null?runResult.getMsg():""));
			log.setHandleTime(new Date());
			//更新日志信息(执行结果),完成任务
			XxlJobCompleter.updateHandleInfoAndFinish(log);
			return new ReturnT<String>(runResult.getMsg());
		} else {
    
    
			return new ReturnT<String>(500, runResult.getMsg());
		}
	}

Before calling the executor to stop execution, first obtain the log record according to the log id, verify whether the current task is still there, and verify whether the executor is scheduled successfully. Only when the scheduling is successful can the call to stop execution be made; obtain the corresponding client according to the address of the executor end, the executor address has been written into the log record when the scheduling is successful, just take out the execution address from the log record, and call the stop method; if the stop task executes successfully, set the executor processing status of the log record to fail, update Log information (execution result), complete the task. Calling the kill method means calling the kill method of the ExecutorBizClient class, and this kill method initiates a remote http call. Source code of the kill method:

    //停止执行器的执行
    @Override
    public ReturnT<String> kill(KillParam killParam) {
    
    
        //发起远程http调用
        return XxlJobRemotingUtil.postBody(addressUrl + "kill", accessToken, timeout, killParam, String.class);
    }

5.log call

​ The log call is for the executor web page to view the execution log file generated by the executor corresponding to a certain log, and perform a task scheduling. The dispatch center will generate a log record and store it in the xxl_job_log table. When the executor processes the task, it will generate Its own execution log, the processing log of the executor exists in a certain directory where the executor is deployed. This log call is to load the execution log file from the executor according to the log id and scheduling time. The storage rule of the execution log file is: default directory/scheduling time (for example, 2023-03-04)/log id.log.

Screenshot of the entry to view the execution log in the web interface:

insert image description here

Screenshots of the interface that displays the execution log and the interface that initiates the request log:

insert image description here

A screenshot of the execution log file stored in the executor:
insert image description here

The interface for loading and executing log files is the logDetailCat interface, see the source code of logDetailCat:

	//查询具体执行明细,需要调用到此任务具体执行的那台机器去获取
	@RequestMapping("/logDetailCat")
	@ResponseBody
	public ReturnT<LogResult> logDetailCat(String executorAddress, long triggerTime, long logId, int fromLineNum){
    
    
		try {
    
    
			ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(executorAddress);
			ReturnT<LogResult> logResult = executorBiz.log(new LogParam(triggerTime, logId, fromLineNum));

			// is end
            if (logResult.getContent()!=null && logResult.getContent().getFromLineNum() > logResult.getContent().getToLineNum()) {
    
    
                XxlJobLog jobLog = xxlJobLogDao.load(logId);
                //处理状态为200表示执行完成,500表示执行异常(也结束),0位未执行完成
                if (jobLog.getHandleCode() > 0) {
    
    
                	//日志已经加载完成
                    logResult.getContent().setEnd(true);
                }
            }

			return logResult;
		} catch (Exception e) {
    
    
			logger.error(e.getMessage(), e);
			return new ReturnT<LogResult>(ReturnT.FAIL_CODE, e.getMessage());
		}
	}

Obtain the client called by the executor according to the address of the executor, initiate a request to the executor, pass the scheduling time, and the log id, so that the executor can splice out the address of the executor file corresponding to the log, and then load the file, and according to the passed The starting line number is loaded; the starting line number is passed in because when viewing the execution log, the executor may not have finished processing. When the executor has not finished processing, the front end uses a timer to call the logDetailCat interface and pass the new If the executor has finished processing, it will return to the front-end a flag that the end is true, and the front-end will no longer call the logDetailCat interface. Calling the log method means calling the log method of the ExecutorBizClient class, and this log method initiates a remote http call. The source code of the log method:

    //查询执行器产生的执行日志信息
    @Override
    public ReturnT<LogResult> log(LogParam logParam) {
    
    
        //发起http调用
        return XxlJobRemotingUtil.postBody(addressUrl + "log", accessToken, timeout, logParam, LogResult.class);
    }

3. Receive request processing

​ The scheduling center will receive the request from the executor. Which requests you receive can be seen from the api class JobApiController in the scheduling center. The JobApiController class is located under the com.xxl.job.admin.controller package, and the included interface processing is callback, registry, and registryRemove Three categories. Look at the source code of the JobApiController class:

@Controller
@RequestMapping("/api")
public class JobApiController {
    
    

    @Resource
    private AdminBiz adminBiz;  //具体类型为AdminBizImpl

    /**
     * api
     *
     * @param uri
     * @param data
     * @return
     */
    @RequestMapping("/{uri}")
    @ResponseBody
    @PermissionLimit(limit=false)
    public ReturnT<String> api(HttpServletRequest request, @PathVariable("uri") String uri, @RequestBody(required = false) String data) {
    
    

        // valid
        //只支持post方式
        if (!"POST".equalsIgnoreCase(request.getMethod())) {
    
    
            return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support.");
        }
        if (uri==null || uri.trim().length()==0) {
    
    
            return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty.");
        }
        //调用调度中心若是配置了token,则需要从执行器的request中获取到token值,在执行器传递token值时使用XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN作为key传递,此处也按这个key取值
        if (XxlJobAdminConfig.getAdminConfig().getAccessToken()!=null
                && XxlJobAdminConfig.getAdminConfig().getAccessToken().trim().length()>0
                && !XxlJobAdminConfig.getAdminConfig().getAccessToken().equals(request.getHeader(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN))) {
    
    
            return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong.");
        }

        // services mapping
        //根据接口的结尾匹配具体是哪个方法
        //callback方法:执行器回调调度中心的方法
        if ("callback".equals(uri)) {
    
    
            List<HandleCallbackParam> callbackParamList = GsonTool.fromJson(data, List.class, HandleCallbackParam.class);
            return adminBiz.callback(callbackParamList);
        } else if ("registry".equals(uri)) {
    
    
            //registry方法:执行器向调度中心进行在线注册的方法,默认30秒调用一次,心跳注册机制
            RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
            return adminBiz.registry(registryParam);
        } else if ("registryRemove".equals(uri)) {
    
    
            //registryRemove方法:执行器结束,在bean销毁的时候会调用销毁执行器在线记录的方法
            RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
            return adminBiz.registryRemove(registryParam);
        } else {
    
    
            return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found.");
        }
    }
}

The JobApiController class uses the interface suffix wildcard to receive the call from the executor. It only supports the post method. If the call scheduling center is configured with a token, it needs to obtain the token value from the request of the executor. When the executor passes the token value, use XxlJobRemotingUtil .XXL_JOB_ACCESS_TOKEN is passed as a key, and the value of this key is also used here; the specific method is matched according to the end of the interface.

​ Handling specific requests is the AdminBiz interface class, which is introduced by injection, indicating that the import is the specific implementation class of the AdminBiz interface, and this class needs to be registered as a bean object. The classes that implement the AdminBiz interface are AdminBizClient and AdminBizImpl. Only the AdminBizImpl class is registered as a bean object (decorated with @Service, @Service annotation and then @Component), so the specific class of AdminBiz injected here is AdminBizImpl.

1. callback request

​The callback request is a method for the executor to feed back the execution result to the scheduling center. After receiving the feedback, the scheduling center updates the execution status and execution message recorded in the log, and ends a task scheduling. If there is a subtask, it will schedule the subtask. Look at the entry source code of the callback request:

        //callback方法:执行器回调调度中心的方法
        if ("callback".equals(uri)) {
    
    
            //接收参数,转成list集合
            List<HandleCallbackParam> callbackParamList = GsonTool.fromJson(data, List.class, HandleCallbackParam.class);
            return adminBiz.callback(callbackParamList);
        }

Receive parameters, turn them into HandleCallbackParam entities, and call the callback method. Look at the source code of the callback method of the AdminBizImpl class:

    //响应执行器反馈的方法
    @Override
    public ReturnT<String> callback(List<HandleCallbackParam> callbackParamList) {
    
    
        //JobCompleteHelper调度中心独有处理类
        return JobCompleteHelper.getInstance().callback(callbackParamList);
    }

The specific class that handles feedback is JobCompleteHelper, see the callback source code of the JobCompleteHelper class:

	public ReturnT<String> callback(List<HandleCallbackParam> callbackParamList) {
    
    
		//使用反馈线程池处理反馈记录
		callbackThreadPool.execute(new Runnable() {
    
    
			@Override
			public void run() {
    
    
                //循环处理所有的执行结果
				for (HandleCallbackParam handleCallbackParam: callbackParamList) {
    
    
					ReturnT<String> callbackResult = callback(handleCallbackParam);
					logger.debug(">>>>>>>>> JobApiController.callback {}, handleCallbackParam={}, callbackResult={}",
							(callbackResult.getCode()== ReturnT.SUCCESS_CODE?"success":"fail"), handleCallbackParam, callbackResult);
				}
			}
		});

		return ReturnT.SUCCESS;
	}

Use the feedback thread pool to process feedback records, process all execution results in a loop, call the callback method in the class, and see the callback source code:

	//调度中心对执行器反馈的处理
	private ReturnT<String> callback(HandleCallbackParam handleCallbackParam) {
    
    
		// valid log item
		//检查日志信息
		XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(handleCallbackParam.getLogId());
		if (log == null) {
    
    
			return new ReturnT<String>(ReturnT.FAIL_CODE, "log item not found.");
		}
		if (log.getHandleCode() > 0) {
    
    
			return new ReturnT<String>(ReturnT.FAIL_CODE, "log repeate callback.");     // avoid repeat callback, trigger child job etc
		}

		// handle msg
		//在原有执行日志的基础上追加上反馈日志
		StringBuffer handleMsg = new StringBuffer();
		if (log.getHandleMsg()!=null) {
    
    
			handleMsg.append(log.getHandleMsg()).append("<br>");
		}
		if (handleCallbackParam.getHandleMsg() != null) {
    
    
			handleMsg.append(handleCallbackParam.getHandleMsg());
		}

		// success, save log
		//设置执行时间、执行结果状态值、执行日志
		log.setHandleTime(new Date());
		log.setHandleCode(handleCallbackParam.getHandleCode());
		log.setHandleMsg(handleMsg.toString());
		//完成此任务,并更新日志的状态值,有子任务再调用子任务
		XxlJobCompleter.updateHandleInfoAndFinish(log);

		return ReturnT.SUCCESS;
	}

This method first checks the log information, adds a feedback log to the original execution log, sets the execution time, execution result status value, and execution log, completes the task, and updates the status value of the log, and then calls the subtask if there are subtasks. Look at the updateHandleInfoAndFinish source code of the method updateHandleInfoAndFinish for completing the task update log:

 public static int updateHandleInfoAndFinish(XxlJobLog xxlJobLog) {
    
    

        // finish
        // 完成此任务,有子任务再调用子任务
        finishJob(xxlJobLog);

        // text最大64kb 避免长度过长
        if (xxlJobLog.getHandleMsg().length() > 15000) {
    
    
            xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg().substring(0, 15000) );
        }

        // fresh handle
        //更新日志的执行器处理情况信息
        return XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateHandleInfo(xxlJobLog);
    }

After completing this task, call subtasks if there are subtasks; update the executor processing information of the log. Look at the completion of the task, if there are subtasks, then schedule the finishJob source code of the subtasks:

  private static void finishJob(XxlJobLog xxlJobLog){
    
    

        // 1、handle success, to trigger child job
        String triggerChildMsg = null;
        //任务执行完成
        if (XxlJobContext.HANDLE_CODE_SUCCESS == xxlJobLog.getHandleCode()) {
    
    
            XxlJobInfo xxlJobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(xxlJobLog.getJobId());
            //查询此任务下是否还有子任务,有子任务则执行子任务
            if (xxlJobInfo!=null && xxlJobInfo.getChildJobId()!=null && xxlJobInfo.getChildJobId().trim().length()>0) {
    
    
                triggerChildMsg = "<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_child_run") +"<<<<<<<<<<< </span><br>";
                //子任务id使用逗号拼接,此处使用逗号进行分割
                String[] childJobIds = xxlJobInfo.getChildJobId().split(",");
                //循环调用子任务执行
                for (int i = 0; i < childJobIds.length; i++) {
    
    
                    //子任务id合法
                    int childJobId = (childJobIds[i]!=null && childJobIds[i].trim().length()>0 && isNumeric(childJobIds[i]))?Integer.valueOf(childJobIds[i]):-1;
                    if (childJobId > 0) {
    
    
                        //执行子任务,调用类型为父类调用
                        JobTriggerPoolHelper.trigger(childJobId, TriggerTypeEnum.PARENT, -1, null, null, null);
                        ReturnT<String> triggerChildResult = ReturnT.SUCCESS;

                        // add msg
                        triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg1"),
                                (i+1),
                                childJobIds.length,
                                childJobIds[i],
                                (triggerChildResult.getCode()==ReturnT.SUCCESS_CODE?I18nUtil.getString("system_success"):I18nUtil.getString("system_fail")),
                                triggerChildResult.getMsg());
                    } else {
    
    
                        triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg2"),
                                (i+1),
                                childJobIds.length,
                                childJobIds[i]);
                    }
                }

            }
        }

        if (triggerChildMsg != null) {
    
    
            xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg() + triggerChildMsg );
        }
    }

If the task execution is completed, check whether there are subtasks under this task. If there are subtasks, execute the subtasks. The subtask ids are concatenated with commas. Here, commas are used to separate them. The subtask id is legal, and the subtasks are executed. The call type is parent class calls. Screenshot of setting subtasks in the web page:
insert image description here

2. registry request

​ The registry requests the behavior of registering online status with the scheduling center for the executor startup or heartbeat mechanism (30 seconds by default), so that the scheduling center can know which executors are online when scheduling, and only online executors can respond to scheduling . Look at the entry source code of the registry request:

else if ("registry".equals(uri)) {
    
    
            //registry方法:执行器向调度中心进行在线注册的方法,默认30秒调用一次,心跳注册机制
            RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
            return adminBiz.registry(registryParam);
        } 

Receive parameters, convert them into RegistryParam entities, and call the registry method. Look at the source code of the registry method of the AdminBizImpl class:

    @Override
    public ReturnT<String> registry(RegistryParam registryParam) {
    
    
        //JobRegistryHelper调度中心独有处理类
        return JobRegistryHelper.getInstance().registry(registryParam);
    }

The specific class that handles feedback is JobRegistryHelper, see the registry source code of the JobRegistryHelper class:

   //响应执行器注册的方法
	public ReturnT<String> registry(RegistryParam registryParam) {
    
    

		// valid
		if (!StringUtils.hasText(registryParam.getRegistryGroup())
				|| !StringUtils.hasText(registryParam.getRegistryKey())
				|| !StringUtils.hasText(registryParam.getRegistryValue())) {
    
    
			return new ReturnT<String>(ReturnT.FAIL_CODE, "Illegal Argument.");
		}

		// async execute
		//使用注册或删除注册的线程池执行
		registryOrRemoveThreadPool.execute(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				//先调用更新的方法
				int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
				if (ret < 1) {
    
    //没有记录,则进行插入
					XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());

					// fresh
					//此方法是一个空方法,刷新执行器的最新在线情况已经由registryMonitorThread守护线程执行
					freshGroupRegistryInfo(registryParam);
				}
			}
		});

		return ReturnT.SUCCESS;
	}

Use the registered or deleted registered thread pool to execute this task. The executor heartbeat registration information is stored in the xxl_job_registry table. When registering, if the record corresponding to the executor already exists, update its latest registration time. If it does not exist, then to insert. Which task group an executor belongs to is determined according to its value in the registry_key field. The registryMonitorThread daemon thread cleans up the xxl_job_registry table once every 30 seconds by default, deletes executor records that have not been registered for more than 90 seconds, and then deletes the latest executor records in xxl_job_registry Group by registry_key, splice the executor address set, and update the latest executor address to the address_list field according to the condition that the registry_key value is equal to the app_name field in the xxl_job_group table. address_list is a collection of current online executor addresses under a certain group, using commas . The screenshot of the online executor collection of the task group is displayed on the web page:
insert image description here

3. registryRemove request

​ When the registryRemove request is for the executor to go offline, it informs the dispatch center that it is offline and needs to remove the executor from the registry, otherwise it cannot respond during scheduling. Look at the entry source code of the registryRemove request:

else if ("registryRemove".equals(uri)) {
    
    
            //registryRemove方法:执行器结束,在bean销毁的时候会调用销毁执行器在线记录的方法
            RegistryParam registryParam = GsonTool.fromJson(data, RegistryParam.class);
            return adminBiz.registryRemove(registryParam);
        }

Receive parameters, convert them into RegistryParam entities, and call the registryRemove method. Look at the source code of the registryRemove method of the AdminBizImpl class:

    @Override
    public ReturnT<String> registryRemove(RegistryParam registryParam) {
    
    
        //JobRegistryHelper调度中心独有处理类
        return JobRegistryHelper.getInstance().registryRemove(registryParam);
    }

The specific class that handles removal is JobRegistryHelper. Look at the registryRemove source code of the JobRegistryHelper class:

	//响应执行器删除注册的方法
	public ReturnT<String> registryRemove(RegistryParam registryParam) {
    
    

		// valid
		if (!StringUtils.hasText(registryParam.getRegistryGroup())
				|| !StringUtils.hasText(registryParam.getRegistryKey())
				|| !StringUtils.hasText(registryParam.getRegistryValue())) {
    
    
			return new ReturnT<String>(ReturnT.FAIL_CODE, "Illegal Argument.");
		}

		// async execute
		//使用注册或删除注册的线程池执行
		registryOrRemoveThreadPool.execute(new Runnable() {
    
    
			@Override
			public void run() {
    
    
				//从执行器注册表中删除记录
				int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryDelete(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue());
				if (ret > 0) {
    
    
					//此方法是一个空方法,刷新执行器的最新在线情况已经由registryMonitorThread守护线程执行
					freshGroupRegistryInfo(registryParam);
				}
			}
		});

		return ReturnT.SUCCESS;
	}

Use the registered or deleted thread pool to perform this processing, and delete the registration record of the executor from xxl_job_registry. When the registryMonitorThread daemon thread processes next time, the online collection of executors will be reorganized, and the executor will be removed at that time.

4. Destruction process at the end of the program

​ When the program starts, 4 thread pools and 6 daemon threads are initialized. When the program ends, these resources need to be destroyed. The entry class for resource destruction is XxlJobAdminConfig.

1. Destroy the entry class

​ The XxlJobAdminConfig class is the entry class for destruction because it implements the DisposableBean interface and overrides the destroy method. When the bean is destroyed, the destroy method will be executed, which can be used as the entry point for the destruction process. Look at the source code related to XxlJobAdminConfig class destruction:

@Component
public class XxlJobAdminConfig implements InitializingBean, DisposableBean {
    
    

    // 实现DisposableBean接口,重写它的bean销毁方法
    @Override
    public void destroy() throws Exception {
    
    
        xxlJobScheduler.destroy();
    }
}

XxlJobAdminConfig is modified by the @Component annotation. When the program starts, it will be loaded into the spring container. At this time, XxlJobAdminConfig is a bean object that implements the DisposableBean interface, which is the bean destruction interface. When the program stops, the bean will be destroyed. In this way, when XxlJobAdminConfig is destroyed, resources can be cleaned up from here.

2. Resource destruction processing

Destroy resources and call destroy() of the XxlJobScheduler class, see the source code of the destroy method:

   //销毁调度中心资源
    public void destroy() throws Exception {
    
    

        // stop-schedule
        //停止预读线程、环形处理任务线程
        JobScheduleHelper.getInstance().toStop();

        // admin log report stop
        //停止日志报表守护线程
        JobLogReportHelper.getInstance().toStop();

        // admin lose-monitor stop
        //销毁处理执行器反馈的线程池、停止没法完成任务监听的守护线程
        JobCompleteHelper.getInstance().toStop();

        // admin fail-monitor stop
        //停止监听失败任务再进行重试调度、发报警邮件的守护线程
        JobFailMonitorHelper.getInstance().toStop();

        // admin registry stop
        //销毁处理执行器注册或者删除的线程池、停止监听执行器是否在线的守护线程
        JobRegistryHelper.getInstance().toStop();

        // admin trigger pool stop
        //销毁处理任务调度的快、慢线程池
        JobTriggerPoolHelper.toStop();

    }

This destroy method is to destroy the thread pool and daemon threads created when initializing init. The following will introduce the destruction process one by one.

(1)JobScheduleHelper.getInstance().toStop()

​ Stop the read-ahead daemon thread and ring processing task daemon thread, see the source code:

    public void toStop(){
    
    

        // 1、stop schedule
        //停止守护线程的while条件
        scheduleThreadToStop = true;
        try {
    
    
            //休眠1秒
            TimeUnit.SECONDS.sleep(1);  // wait
        } catch (InterruptedException e) {
    
    
            logger.error(e.getMessage(), e);
        }
        //中断线程
        if (scheduleThread.getState() != Thread.State.TERMINATED){
    
    
            // interrupt and wait
            scheduleThread.interrupt();
            try {
    
    
                scheduleThread.join();
            } catch (InterruptedException e) {
    
    
                logger.error(e.getMessage(), e);
            }
        }

        // if has ring data
        //是否还有未处理完的环形预处理任务
        boolean hasRingData = false;
        if (!ringData.isEmpty()) {
    
    
            for (int second : ringData.keySet()) {
    
    
                List<Integer> tmpData = ringData.get(second);
                if (tmpData!=null && tmpData.size()>0) {
    
    
                    hasRingData = true;
                    break;
                }
            }
        }
        //有未处理完的预读任务
        if (hasRingData) {
    
    
            try {
    
    
                //休眠8秒,让预读处理任务处理完成
                TimeUnit.SECONDS.sleep(8);
            } catch (InterruptedException e) {
    
    
                logger.error(e.getMessage(), e);
            }
        }

        // stop ring (wait job-in-memory stop)
        //停止环形任务线程的while条件
        ringThreadToStop = true;
        try {
    
    
            //休眠一秒
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
    
            logger.error(e.getMessage(), e);
        }
        //中断环形预处理线程
        if (ringThread.getState() != Thread.State.TERMINATED){
    
    
            // interrupt and wait
            ringThread.interrupt();
            try {
    
    
                ringThread.join();
            } catch (InterruptedException e) {
    
    
                logger.error(e.getMessage(), e);
            }
        }

        logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper stop");
    }

The while condition for stopping the daemon thread, scheduleThreadToStop is modified by volatile, and the field modified by volatile has a bus sniffing perception mechanism. When the value of scheduleThreadToStop is changed in a thread, the changed result value will be written to the main thread in time , and then other threads that refer to this variable will perceive the change, invalidate the value of this variable in their own copy, and read the latest value again. If there are unfinished preprocessing tasks, let the program sleep for 8 seconds. 8 seconds is enough to process the pre-reading tasks, because the pre-reading tasks are within 5 seconds.

(2)JobLogReportHelper.getInstance().toStop()

​ Stop the log report daemon thread, see the source code:

  public void toStop(){
    
    
      //跳出while语句
        toStop = true;
        // interrupt and wait
       //中断线程
        logrThread.interrupt();
        try {
    
    
            //等待线程处理完成
            logrThread.join();
        } catch (InterruptedException e) {
    
    
            logger.error(e.getMessage(), e);
        }
    }

(3)JobCompleteHelper.getInstance().toStop()

​ Destroy the thread pool that handles the executor callback, stop the daemon thread that cannot complete the task monitoring, see the source code:

	public void toStop(){
    
    
        //跳出while语句
		toStop = true;

		// stop registryOrRemoveThreadPool
		//销毁线程池
		callbackThreadPool.shutdownNow();

		// stop monitorThread (interrupt and wait)
        //中断线程
		monitorThread.interrupt();
		try {
    
    
			monitorThread.join();
		} catch (InterruptedException e) {
    
    
			logger.error(e.getMessage(), e);
		}
	}

(4)JobFailMonitorHelper.getInstance().toStop()

​ Stop listening to failed tasks, retry scheduling, and send alarm emails to the daemon thread, see the source code:

	public void toStop(){
    
    
        //跳出while语句
		toStop = true;
		// interrupt and wait
         //中断线程
		monitorThread.interrupt();
		try {
    
    
			monitorThread.join();
		} catch (InterruptedException e) {
    
    
			logger.error(e.getMessage(), e);
		}
	}

(5)JobRegistryHelper.getInstance().toStop()

​ Destroy the thread pool that handles the registration or deletion of the executor, and stop the daemon thread that monitors whether the executor is online. See the source code:

	public void toStop(){
    
    
        //跳出while语句
		toStop = true;

		// stop registryOrRemoveThreadPool
		//销毁线程池
		registryOrRemoveThreadPool.shutdownNow();

		// stop monitir (interrupt and wait)
        //中断线程
		registryMonitorThread.interrupt();
		try {
    
    
			registryMonitorThread.join();
		} catch (InterruptedException e) {
    
    
			logger.error(e.getMessage(), e);
		}
	}

(6)JobTriggerPoolHelper.toStop()

​ Destroy the fast and slow thread pools that handle task scheduling, see the source code:

  public static void toStop() {
    
    
     helper.stop();
  }

  public void stop() {
    
    
        //triggerPool.shutdown();
        //销毁线程池
        fastTriggerPool.shutdownNow();
        slowTriggerPool.shutdownNow();
        logger.info(">>>>>>>>> xxl-job trigger thread pool shutdown success.");
    }

Actuator

​ The executor is the specific execution of the task and the specific implementation of the task logic processing. It provides a response to the scheduling of the dispatch center, and also actively initiates a request to the dispatch center. An executor is generally a system for developing business codes. Some modules need to use the timer processing function. The project introduces xxl-job-core dependencies and can be used as an executor for development.

1. Program startup initialization

After the program starts, it will do a lot of resource initialization, initialize netty to monitor a certain port, and the dispatch center will call the port address of this netty to establish a connection with the actuator. The entry class for resource initialization is the XxlJobSpringExecutor class, and we look at the initialization process from this class.

1. Initialize the entry class

​ The reason why the entry class is XxlJobSpringExecutor is because when we configure the xxl-job configuration file on the actuator side, we use the XxlJobSpringExecutor entity to receive configuration information, and register XxlJobSpringExecutor as a Bean object. With the Bean object, we initialize the resource You can start here. Let’s take a look at the configuration class source code of the executor:

@Configuration
public class XxlJobConfig {
    
    
    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

    //调度中心地址
    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    //token值
    @Value("${xxl.job.accessToken}")
    private String accessToken;

    //所属的执行器分组
    @Value("${xxl.job.executor.appname}")
    private String appname;

    //执行器地址
    @Value("${xxl.job.executor.address}")
    private String address;

    //ip
    @Value("${xxl.job.executor.ip}")
    private String ip;

    //netty监听的端口
    @Value("${xxl.job.executor.port}")
    private int port;

    //执行日志存放的目录
    @Value("${xxl.job.executor.logpath}")
    private String logPath;

    //执行日志最多存放天数
    @Value("${xxl.job.executor.logretentiondays}")
    private int logRetentionDays;

    //注册xxlJobExecutor的bean
    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
    
    
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setAddress(address);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    }
}

This configuration class receives the configuration information in the application.properties configuration file, creates a XxlJobSpringExecutor entity class, assigns all the received configuration information to this entity class, and registers the XxlJobSpringExecutor entity as a Bean object. Let's take a look at the XxlJobSpringExecutor entity source code:

public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, SmartInitializingSingleton, DisposableBean {
    
    
    private static final Logger logger = LoggerFactory.getLogger(XxlJobSpringExecutor.class);


    // 实现了SmartInitializingSingleton接口(只适用于单列bean),在bean实例初始化完成后,会调用afterSingletonsInstantiated方法
    @Override
    public void afterSingletonsInstantiated() {
    
    

        // init JobHandler Repository
        /*initJobHandlerRepository(applicationContext);*/

        // init JobHandler Repository (for method)
        //初始化任务方法,处理所有Bean中使用@XxlJob注解标识的方法
        initJobHandlerMethodRepository(applicationContext);

        // refresh GlueFactory
        //重新设置GlueFactory的类型为SpringGlueFactory
        GlueFactory.refreshInstance(1);

        // super start
        try {
    
    
            //调用到XxlJobExecutor类的start方法,对一些资源进行初始化
            super.start();
        } catch (Exception e) {
    
    
            throw new RuntimeException(e);
        }
    }

    // 实现DisposableBean接口,重写它的bean销毁方法
    @Override
    public void destroy() {
    
    
        super.destroy();
    }

    private void initJobHandlerMethodRepository(ApplicationContext applicationContext) {
    
    
        if (applicationContext == null) {
    
    
            return;
        }
        // init job handler from method
        //从程序上下文中获取到所有的bean名称集合
        String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class, false, true);
        //遍历bean集合
        for (String beanDefinitionName : beanDefinitionNames) {
    
    
            //根据bean名称从程序上下文获取到此bean对象
            Object bean = applicationContext.getBean(beanDefinitionName);

            Map<Method, XxlJob> annotatedMethods = null;   // referred to :org.springframework.context.event.EventListenerMethodProcessor.processBean
            try {
    
    
                //对Bean对象进行方法过滤,查询到方法被XxlJob注解修饰,是则放到annotatedMethods集合中
                annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
                        new MethodIntrospector.MetadataLookup<XxlJob>() {
    
    
                            @Override
                            public XxlJob inspect(Method method) {
    
    
                                //判断方法被XxlJob注解修饰才返回
                                return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
                            }
                        });
            } catch (Throwable ex) {
    
    
                logger.error("xxl-job method-jobhandler resolve error for bean[" + beanDefinitionName + "].", ex);
            }
            //当前遍历的bean没有被XxlJob注解修饰,则调过处理
            if (annotatedMethods==null || annotatedMethods.isEmpty()) {
    
    
                continue;
            }

            //循环处理当前Bean下被XxlJob修饰的方法
            for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
    
    
                //执行的方法
                Method executeMethod = methodXxlJobEntry.getKey();
                //XxlJob注解类
                XxlJob xxlJob = methodXxlJobEntry.getValue();
                // regist
                //注册此任务处理器
                registJobHandler(xxlJob, bean, executeMethod);
            }
        }
    }

    // ---------------------- applicationContext ----------------------
    private static ApplicationContext applicationContext;

    //实现ApplicationContextAware接口,获取上下文,得到加载到spring容器中的所有bean对象
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    
    
        XxlJobSpringExecutor.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
    
    
        return applicationContext;
    }
}

The XxlJobSpringExecutor class inherits the XxlJobExecutor class, and the fields for receiving configuration information are defined in the XxlJobExecutor class; the XxlJobSpringExecutor class implements the ApplicationContextAware interface, and the spring context can be obtained by rewriting its setApplicationContext method; it implements the SmartInitializingSingleton interface, and rewrites its afterSingletonsInstantiated method, after the bean instance is initialized, the afterSingletonsInstantiated method will be called, and this method is the real entry point for initialization.

2. Initialize the method of processing tasks

​ When the scheduling center adds a task, it needs to specify which task processing method of the executor to execute the task, because multiple methods of processing tasks can be defined in an executor. Screenshot of the method of configuring and processing tasks on the web interface of the dispatch center:
insert image description here

The executor uses the @XxlJob annotation to decorate each method of processing tasks. The @XxlJob annotation provides three optional configurations. The value value is used to match the JobHandler value when the dispatch center creates the task. init is the initial method before configuring the task, destroy It is the destruction work after the configuration processing task. Look at the @XxlJob source code:

@Target({
    
    ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface XxlJob {
    
    

    /**
     * jobhandler name
     */
    String value();

    /**
     * init handler, invoked when JobThread init
     */
    String init() default "";

    /**
     * destroy handler, invoked when JobThread destroy
     */
    String destroy() default "";
}

If you want to get the configured task processing methods when the program starts, that is, the method modified by @XxlJob, you need to set the class of the configuration task as a Bean object, that is, use @Component to modify it, so that you can find methods based on the Bean object The method containing the @XxlJob annotation is stored in a collection. When receiving the task execution command from the dispatch center, you can find its corresponding method from this collection, and then execute this method. Look at the source code for configuring specific processing tasks:

@Component
public class SampleXxlJob {
    
    
    private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class);

    /**
     * 1、简单任务示例(Bean模式)
     */
    @XxlJob("demoJobHandler")
    public void demoJobHandler() throws Exception {
    
    
        XxlJobHelper.log("XXL-JOB, Hello World.");

        for (int i = 0; i < 5; i++) {
    
    
            XxlJobHelper.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        // default success
    }


    /**
     * 2、分片广播任务
     */
    @XxlJob("shardingJobHandler")
    public void shardingJobHandler() throws Exception {
    
    

        // 分片参数
        int shardIndex = XxlJobHelper.getShardIndex();
        int shardTotal = XxlJobHelper.getShardTotal();

        XxlJobHelper.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal);

        // 业务逻辑
        for (int i = 0; i < shardTotal; i++) {
    
    
            if (i == shardIndex) {
    
    
                XxlJobHelper.log("第 {} 片, 命中分片开始处理", i);
            } else {
    
    
                XxlJobHelper.log("第 {} 片, 忽略", i);
            }
        }

    }


    /**
     * 生命周期任务示例:任务初始化与销毁时,支持自定义相关逻辑;
     */
    @XxlJob(value = "demoJobHandler2", init = "init", destroy = "destroy")
    public void demoJobHandler2() throws Exception {
    
    
        XxlJobHelper.log("XXL-JOB, Hello World.");
    }
    public void init(){
    
    
        logger.info("init");
    }
    public void destroy(){
    
    
        logger.info("destroy");
    }
}

The entry to handle the method modified by @XxlJob is: the XxlJobSpringExecutor class implements the SmartInitializingSingleton interface and rewrites its afterSingletonsInstantiated method. This method will be executed when the Bean object is instantiated and initialized. This method contains the method initJobHandlerMethodRepository for parsing @XxlJob. See this The source code of the method:

  private void initJobHandlerMethodRepository(ApplicationContext applicationContext) {
    
    
        if (applicationContext == null) {
    
    
            return;
        }
        // init job handler from method
        //从程序上下文中获取到所有的bean名称集合
        String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class, false, true);
        //遍历bean集合
        for (String beanDefinitionName : beanDefinitionNames) {
    
    
            //根据bean名称从程序上下文获取到此bean对象
            Object bean = applicationContext.getBean(beanDefinitionName);

            Map<Method, XxlJob> annotatedMethods = null;   // referred to :org.springframework.context.event.EventListenerMethodProcessor.processBean
            try {
    
    
                //对Bean对象进行方法过滤,查询到方法被XxlJob注解修饰,是则放到annotatedMethods集合中
                annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
                        new MethodIntrospector.MetadataLookup<XxlJob>() {
    
    
                            @Override
                            public XxlJob inspect(Method method) {
    
    
                                //判断方法被XxlJob注解修饰才返回
                                return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
                            }
                        });
            } catch (Throwable ex) {
    
    
                logger.error("xxl-job method-jobhandler resolve error for bean[" + beanDefinitionName + "].", ex);
            }
            //当前遍历的bean没有被XxlJob注解修饰,则调过处理
            if (annotatedMethods==null || annotatedMethods.isEmpty()) {
    
    
                continue;
            }

            //循环处理当前Bean下被XxlJob修饰的方法
            for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
    
    
                //执行的方法
                Method executeMethod = methodXxlJobEntry.getKey();
                //XxlJob注解类
                XxlJob xxlJob = methodXxlJobEntry.getValue();
                // regist
                //注册此任务处理器
                registJobHandler(xxlJob, bean, executeMethod);
            }
        }
    }

This method obtains all bean name collections from the program context, traverses the bean collection, obtains the bean object from the program context according to the bean name, performs method filtering on the Bean object, queries the records whose methods are modified by XxlJob annotations, and puts them in annotatedMethods In the collection, process the method modified by XxlJob under the current Bean in a loop, and register this task processing method. The registration task processing method is registJobHandler, see the source code of registJobHandler:

 protected void registJobHandler(XxlJob xxlJob, Object bean, Method executeMethod){
    
    
        if (xxlJob == null) {
    
    
            return;
        }

        //获取注解@XxlJob("demoJobHandler")配置的值
        String name = xxlJob.value();
        //make and simplify the variables since they'll be called several times later
        //获取此Bean对象的类
        Class<?> clazz = bean.getClass();
        //获取方法名称
        String methodName = executeMethod.getName();
        if (name.trim().length() == 0) {
    
    
            throw new RuntimeException("xxl-job method-jobhandler name invalid, for[" + clazz + "#" + methodName + "] .");
        }
        //判断是否已经有名称为name值的@XxlJob
        if (loadJobHandler(name) != null) {
    
    
            throw new RuntimeException("xxl-job jobhandler[" + name + "] naming conflicts.");
        }

        //方法关闭安全检查
        executeMethod.setAccessible(true);

        // init and destroy
        Method initMethod = null;
        Method destroyMethod = null;

        //注解XxlJob是否有配置init属性
        if (xxlJob.init().trim().length() > 0) {
    
    
            try {
    
    
                //通过反射机制获取到init方法
                initMethod = clazz.getDeclaredMethod(xxlJob.init());
                initMethod.setAccessible(true);
            } catch (NoSuchMethodException e) {
    
    
                throw new RuntimeException("xxl-job method-jobhandler initMethod invalid, for[" + clazz + "#" + methodName + "] .");
            }
        }
        //注解XxlJob是否有配置destroy属性
        if (xxlJob.destroy().trim().length() > 0) {
    
    
            try {
    
    
                //通过反射机制获取到destroy方法
                destroyMethod = clazz.getDeclaredMethod(xxlJob.destroy());
                destroyMethod.setAccessible(true);
            } catch (NoSuchMethodException e) {
    
    
                throw new RuntimeException("xxl-job method-jobhandler destroyMethod invalid, for[" + clazz + "#" + methodName + "] .");
            }
        }

        // registry jobhandler
        // 把此被XxlJob注解修饰的方法注册到任务处理器中,new MethodJobHandler创建一个任务处理器方法
        registJobHandler(name, new MethodJobHandler(bean, executeMethod, initMethod, destroyMethod));

    }

This method obtains the value configured by the annotation @XxlJob. This value corresponds to the JobHandler of the scheduling center. This value is unique within an executor. Check whether the init and destroy attributes are configured. If so, use the Bean reflection mechanism to obtain to a specific method. For these parsed values, use the MethodJobHandler entity to store them, and then add this processor to a map collection, the key is the value configured by @XxlJob, and the value is the created MethodJobHandler entity. Look at the specific registered registJobHandler source code:

 //job处理器集合,key:@XxlJob注解的value值,value:此任务执行的对象,包含Bean对象,执行的方法、初始方法、销毁方法
    private static ConcurrentMap<String, IJobHandler> jobHandlerRepository = new ConcurrentHashMap<String, IJobHandler>();

    public static IJobHandler loadJobHandler(String name){
    
    
        return jobHandlerRepository.get(name);
    }

    public static IJobHandler registJobHandler(String name, IJobHandler jobHandler){
    
    
        logger.info(">>>>>>>>>>> xxl-job register jobhandler success, name:{}, jobHandler:{}", name, jobHandler);
        //把任务添加处理器集合中,后续当需要处理某个@XxlJob定义的任务时,直接从jobHandlerRepository集合按key取出,直接调用它的执行方法即可
        return jobHandlerRepository.put(name, jobHandler);
    }

Use ConcurrentMap to store these processing methods. When the executor is scheduled, the specific processing class is obtained from this map according to the JobHandler configured by the task as the key.

3. Initialize the execution log directory

​ In the afterSingletonsInstantiated() method, the method super.start() of the initial other resources is called. Because the initialization entry class XxlJobSpringExecutor inherits XxlJobExecutor, this start method calls the method of the XxlJobExecutor class. Look at the source code of the start method:

   public void start() throws Exception {
    
    

        // init logpath
        //初始化执行日志目录
        XxlJobFileAppender.initLogPath(logPath);

        // init invoker, admin-client
        //初始化操作调度中心的客户端
        initAdminBizList(adminAddresses, accessToken);


        // init JobLogFileCleanThread
        //初始化清除日志文件的守护线程,清除周期为1天,按配置的保留文件天数进行过期文件的清除
        JobLogFileCleanThread.getInstance().start(logRetentionDays);

        // init TriggerCallbackThread
        //初始化调度反馈线程,若反馈阻塞队列有值,则进行反馈,并把反馈结果写入日志文件中;若是反馈失败,则把记录写入到反馈失败日志中;初始化一个重试失败反馈的线程进行失败心跳重试反馈
        TriggerCallbackThread.getInstance().start();

        // init executor-server
        //初始化netty服务,监听端口的调用情况,做出响应处理;把当前执行器注册到调度中心中,初始化一个注册线程,并指定时间进行心跳调用注册方法
        initEmbedServer(address, ip, port, appname, accessToken);
    }

The start method includes many other initialization methods. The required parameters such as logPath and adminAddresses have been assigned values ​​when creating the XxlJobSpringExecutor class. The source of these values ​​is the value configured in application.properties. Look at the source code of the initLogPath method for initializing the execution log directory:

    private static String logBasePath = "/data/applogs/xxl-job/jobhandler";
	private static String glueSrcPath = logBasePath.concat("/gluesource");
	//初始化执行日志目录
	public static void initLogPath(String logPath){
    
    
		// init
		if (logPath!=null && logPath.trim().length()>0) {
    
    
			logBasePath = logPath;
		}
		// mk base dir
		//创建目录
		File logPathDir = new File(logBasePath);
		//不存在则进行创建
		if (!logPathDir.exists()) {
    
    
			logPathDir.mkdirs();
		}
		logBasePath = logPathDir.getPath();

		// mk glue dir
		//创建目录,
		File glueBaseDir = new File(logPathDir, "gluesource");
		//不存在则进行创建
		if (!glueBaseDir.exists()) {
    
    
			glueBaseDir.mkdirs();
		}
		glueSrcPath = glueBaseDir.getPath();
	}
	public static String getLogPath() {
    
    
		return logBasePath;
	}
	public static String getGlueSrcPath() {
    
    
		return glueSrcPath;
	}

Use logBasePath to record the storage directory of the execution log, and glueSrcPath to record the directory that needs to organize the running script into a file such as bash.sh in shell mode. If the user configures the logPath directory, the logBasePath will be overwritten. If there is no configuration, the default directory will be used.

Execution log storage screenshot:
insert image description here

Run file storage screenshot:
insert image description here

4. Initialize the client of the operation dispatch center

​ The executor needs to interact with the dispatch center. The dispatch center may be deployed in a cluster, so it is necessary to use a collection to store all the dispatch center clients. The executor calls the dispatch center and needs to know the address and token value, so the client class stores the dispatch center The address, token, and token value must be consistent with those saved in the dispatch center configuration, otherwise, the verification will fail when dispatching. Here, the client of the operation scheduling center is initialized, and when it needs to be called later, it can directly take the client to initiate the request. Look at the source code of the method initAdminBizList for initializing the dispatch center:

    //存放所有的调用调度中心的客户端
    private static List<AdminBiz> adminBizList;
    //初始化连接调度中心的客户端
    private void initAdminBizList(String adminAddresses, String accessToken) throws Exception {
    
    
        //调度中心的地址使用逗号进行分割
        if (adminAddresses!=null && adminAddresses.trim().length()>0) {
    
    
            for (String address: adminAddresses.trim().split(",")) {
    
    
                if (address!=null && address.trim().length()>0) {
    
    
                    //创建调用调度中心的客户端
                    AdminBiz adminBiz = new AdminBizClient(address.trim(), accessToken);

                    if (adminBizList == null) {
    
    
                        adminBizList = new ArrayList<AdminBiz>();
                    }
                    adminBizList.add(adminBiz);
                }
            }
        }
    }

    public static List<AdminBiz> getAdminBizList(){
    
    
        return adminBizList;
    }

adminAddresses is the address of the dispatch center defined in the application.properties configuration file. When the dispatch center is a cluster, use commas to connect. Here, use commas to separate, create all dispatch center client classes AdminBizClient, and store them in the list collection .

5. Initialize the daemon thread that clears the log file

​ Execution log files support setting the number of days to save. The number of days here needs to be consistent with the number of days to save the log record set by the dispatch center, otherwise the dispatch center will not be able to find it when viewing the execution log of a certain log record. Here, the log record is the record stored in the table xxl_job_log by the scheduling center, and the execution log is the task file generated by the execution of the task by the executor, which is stored in the directory of the executor deployment server. Look at the source code of the start method initialized under the JobLogFileCleanThread class:

    public void start(final long logRetentionDays){
    
    

        // limit min value
        //日志存留天数需要大于3才有效果
        if (logRetentionDays < 3 ) {
    
    
            return;
        }
        //创建一个线程
        localThread = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                //没有停止线程
                while (!toStop) {
    
    
                    try {
    
    
                        // clean log dir, over logRetentionDays
                        //获取磁盘下的日志文件集合
                        File[] childDirs = new File(XxlJobFileAppender.getLogPath()).listFiles();
                        if (childDirs!=null && childDirs.length>0) {
    
    

                            // today
                            //获取今天的时间
                            Calendar todayCal = Calendar.getInstance();
                            todayCal.set(Calendar.HOUR_OF_DAY,0);
                            todayCal.set(Calendar.MINUTE,0);
                            todayCal.set(Calendar.SECOND,0);
                            todayCal.set(Calendar.MILLISECOND,0);

                            Date todayDate = todayCal.getTime();

                            for (File childFile: childDirs) {
    
    

                                // valid
                                //判断是否为目录,日志文件是按日期存放的,例:2023-02-25/1.log
                                if (!childFile.isDirectory()) {
    
    
                                    continue;
                                }
                                //文件需要包含-
                                if (childFile.getName().indexOf("-") == -1) {
    
    
                                    continue;
                                }

                                // file create date
                                Date logFileCreateDate = null;
                                try {
    
    
                                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
                                    //把以日期格式命名的文件名转成日期格式
                                    logFileCreateDate = simpleDateFormat.parse(childFile.getName());
                                } catch (ParseException e) {
    
    
                                    logger.error(e.getMessage(), e);
                                }
                                if (logFileCreateDate == null) {
    
    
                                    continue;
                                }
                                //当前时间-文件创建时间的差值(毫秒)大于logRetentionDays指定的日志保留天数,logRetentionDays * (24 * 60 * 60 * 1000)是把天数转成毫秒
                                if ((todayDate.getTime()-logFileCreateDate.getTime()) >= logRetentionDays * (24 * 60 * 60 * 1000) ) {
    
    
                                    //递归删除此文件夹及它下面的文件
                                    FileUtil.deleteRecursively(childFile);
                                }

                            }
                        }

                    } catch (Exception e) {
    
    
                        if (!toStop) {
    
    
                            logger.error(e.getMessage(), e);
                        }

                    }

                    try {
    
    
                        //休眠一天时间
                        TimeUnit.DAYS.sleep(1);
                    } catch (InterruptedException e) {
    
    
                        if (!toStop) {
    
    
                            logger.error(e.getMessage(), e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, executor JobLogFileCleanThread thread destroy.");

            }
        });
        //设置为守护线程,
        localThread.setDaemon(true);
        localThread.setName("xxl-job, executor JobLogFileCleanThread");
        //启动守护线程
        localThread.start();
    }

The configured log storage days must be greater than 3 to take effect. Create a daemon thread with a sleep cycle of 1 day to judge and clean up in a loop. Get the log file collection under the disk every time you clean up. The log file storage format is: 2023-02-25 /1.log, a log date folder is added to the outer layer of the specific log, only the folder is processed here, and the folder name is judged to be in a date format (including -), and then the folder name is converted to a date, using the current date Subtract the date when the folder was converted, if the difference is greater than the number of days to be saved, recursively delete this folder and the files under it.

6. Initialize the daemon thread that feeds back the execution result to the dispatch center

​ After the scheduling center calls the execution method of the executor, the executor does not execute the task immediately. It first returns the scheduling success to the calling center, and adds the scheduling task to the queue. When the task is taken out by the execution thread and executed, the execution is executed. The result is put into the feedback queue, and this daemon thread is to feed back the feedback information in the feedback queue to the dispatch center. Look at the start source code of the TriggerCallbackThread class initialization feedback execution result daemon thread:

  public void start() {
    
    

        // valid
        //检查是否有调用调度中心的客户端
        if (XxlJobExecutor.getAdminBizList() == null) {
    
    
            logger.warn(">>>>>>>>>>> xxl-job, executor callback config fail, adminAddresses is null.");
            return;
        }

        // callback
        //创建反馈回调线程
        triggerCallbackThread = new Thread(new Runnable() {
    
    

            @Override
            public void run() {
    
    

                // normal callback
                //只要不停止,就一直循环获取
                while(!toStop){
    
    
                    try {
    
    
                        //使用take方法出队,take和put方法不互斥,读写分离,分别使用takeLock/putLock进行加锁
                        HandleCallbackParam callback = getInstance().callBackQueue.take();
                        //回调参数类不为空,则处理回调
                        if (callback != null) {
    
    

                            // callback list param
                            //定义一个集合接收callBackQueue队列中的所有回调类
                            List<HandleCallbackParam> callbackParamList = new ArrayList<HandleCallbackParam>();
                            //drainTo方法为把callBackQueue队列中的所有值转移到新的callbackParamList集合中,经过此方法调用,此时callBackQueue为空,callbackParamList接收到队列里面的所有元素
                            int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList);
                            //一开始出队列的的对象也要加入到集合中
                            callbackParamList.add(callback);

                            // callback, will retry if error
                            if (callbackParamList!=null && callbackParamList.size()>0) {
    
    
                                //处理回调
                                doCallback(callbackParamList);
                            }
                        }
                    } catch (Exception e) {
    
    
                        if (!toStop) {
    
    
                            logger.error(e.getMessage(), e);
                        }
                    }
                }

                // last callback
                //当停止反馈线程后,把当前callBackQueue反馈队列里面还没有反馈完的记录进行反馈
                try {
    
    
                    List<HandleCallbackParam> callbackParamList = new ArrayList<HandleCallbackParam>();
                    int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList);
                    if (callbackParamList!=null && callbackParamList.size()>0) {
    
    
                        doCallback(callbackParamList);
                    }
                } catch (Exception e) {
    
    
                    if (!toStop) {
    
    
                        logger.error(e.getMessage(), e);
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, executor callback thread destroy.");

            }
        });
        //设置为守护线程
        triggerCallbackThread.setDaemon(true);
        triggerCallbackThread.setName("xxl-job, executor TriggerCallbackThread");
        //启动线程
        triggerCallbackThread.start();
}

If there is no call to the scheduling center client, no thread creation will be performed. The feedback daemon thread has no sleep cycle, and it keeps cyclically taking values ​​from the feedback queue callBackQueue. When there is a feedback record, the feedback is directly executed. When the feedback thread is stopped, the current callBackQueue feedback queue Feedback for records that have not been fed back.

7. Initialize the daemon thread of the retry feedback failure record

​ When the executor feeds back the execution results to the dispatch center, there may be network problems or the restart of the dispatch center, resulting in a feedback failure. The record of the feedback failure will be placed in the feedback failure directory file of the executor, and the address definition source code of the feedback failure file is stored:

    //回调失败日志目录
    private static String failCallbackFilePath = XxlJobFileAppender.getLogPath().concat(File.separator).concat("callbacklog").concat(File.separator);
    //回调失败日志文件名
    private static String failCallbackFileName = failCallbackFilePath.concat("xxl-job-callback-{x}").concat(".log");

Add the callbacklog directory under the log directory of the executor configuration, and the log file name is in the format of .log. Look at the source code of the TriggerCallbackThread class to initialize this daemon thread:

   public void start() {
    
    

        // valid
        //检查是否有调用调度中心的客户端
        if (XxlJobExecutor.getAdminBizList() == null) {
    
    
            logger.warn(">>>>>>>>>>> xxl-job, executor callback config fail, adminAddresses is null.");
            return;
        }

        // retry
        //重试回调上面回调线程triggerCallbackThread调用失败的记录,按休眠时间进行循环
        triggerRetryCallbackThread = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                while(!toStop){
    
    
                    try {
    
    
                        //重试反馈一开始进行反馈并失败的记录
                        retryFailCallbackFile();
                    } catch (Exception e) {
    
    
                        if (!toStop) {
    
    
                            logger.error(e.getMessage(), e);
                        }

                    }
                    try {
    
    
                        //默认休眠30秒
                        TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
                    } catch (InterruptedException e) {
    
    
                        if (!toStop) {
    
    
                            logger.error(e.getMessage(), e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, executor retry callback thread destroy.");
            }
        });
        //设置为守护线程
        triggerRetryCallbackThread.setDaemon(true);
        //启动线程
        triggerRetryCallbackThread.start();

    }

If the dispatch center client is not called, no thread creation will be performed. The retry failure feedback guard will sleep for 30 seconds by default, and the retry failure feedback method retryFailCallbackFile will be called cyclically. See its source code:

    //重试反馈一开始进行反馈并失败的记录
    private void retryFailCallbackFile(){
    
    

        // valid
        //检查存放失败反馈的文件目录是否为空
        File callbackLogPath = new File(failCallbackFilePath);
        if (!callbackLogPath.exists()) {
    
    
            return;
        }
        //callbackLogPath是一个目录,若是一个文件,则删除此文件
        if (callbackLogPath.isFile()) {
    
    
            callbackLogPath.delete();
        }
        //callbackLogPath是一个目录、并且此目录下有文件才放行
        if (!(callbackLogPath.isDirectory() && callbackLogPath.list()!=null && callbackLogPath.list().length>0)) {
    
    
            return;
        }

        // load and clear file, retry
        //遍历处理回调错误日志
        for (File callbaclLogFile: callbackLogPath.listFiles()) {
    
    
            //把文件转成byte数组
            byte[] callbackParamList_bytes = FileUtil.readFileContent(callbaclLogFile);

            // avoid empty file
            //若是空文件则删除
            if(callbackParamList_bytes == null || callbackParamList_bytes.length < 1){
    
    
                callbaclLogFile.delete();
                continue;
            }
            //把byte数组转成list集合,一开始就是把list集合转成byte数组存放到文件中的,现在就是反向转一下
            List<HandleCallbackParam> callbackParamList = (List<HandleCallbackParam>) JdkSerializeTool.deserialize(callbackParamList_bytes, List.class);
            //删除文件
            callbaclLogFile.delete();
            //调用反馈的方法
            doCallback(callbackParamList);
        }

    }

Check whether the file directory storing the failure feedback is empty. If it is not empty, traverse and process the feedback failure log. If it is empty, delete the file. Read the byte array from the log file and convert the byte array into a list collection. At the beginning, the list collection is Converted into a byte array and stored in the file, now it is reversed, and after getting the record, call the feedback method.

8. Initialize the daemon thread and create a thread pool for netty service listening port calls + processing calls

​ The executor uses the netty service to receive calls from the dispatch center. netty is an excellent asynchronous, event-driven network application framework. Netty receives the call and uses the thread pool to handle the specific implementation of the call. Let's look at the source code of the initialized entry initEmbedServer method:

  private void initEmbedServer(String address, String ip, int port, String appname, String accessToken) throws Exception {
    
    

        // fill ip port
        //监听端口,有配置则使用配置的端口,没有配置,则查找一个没有被占用的端口
        port = port>0?port: NetUtil.findAvailablePort(9999);
        //执行器的ip,有配置则使用配置,没有配置,则获取本地ip地址
        ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();

        // generate address
        //若是本地机器的地址没有配置,则使用上面获取到的本地ip、本地端口组织address;有配置则使用配置的地址
        if (address==null || address.trim().length()==0) {
    
    
            //得到ip:端口的连接信息
            String ip_port_address = IpUtil.getIpPort(ip, port);   // registry-address:default use address to registry , otherwise use ip:port if address is null
            //组织address地址信息
            address = "http://{ip_port}/".replace("{ip_port}", ip_port_address);
        }

        // accessToken
        //没有加token信息,则输出警告日志信息
        if (accessToken==null || accessToken.trim().length()==0) {
    
    
            logger.warn(">>>>>>>>>>> xxl-job accessToken is empty. To ensure system security, please set the accessToken.");
        }

        // start
        //创建一个基于netty的监听服务器,监听port端口
        embedServer = new EmbedServer();
        //启动此监听服务器,创建一个守护线程,创建一个netty服务,监听port端口,创建一个自定义处理器来处理netty服务被调用时的响应处理类;
        //使用线程池来处理netty的服务调用,根据服务请求的uri来具体处理调用请求,处理结束后,向调用方响应处理结果
        //把当前执行器注册到调度中心中
        embedServer.start(address, port, appname, accessToken);
    }

The executor's own ip and netty port support configuration in the application.properties configuration file. If no ip is configured, it supports obtaining its own ip address, and supports ipv4 and ipv6 networks; if the port monitored by netty is not configured, it starts from 9999 Use linear probing between 65535 and find an unoccupied port. Note that the port of netty is not the same as the port of the executor project. Create an EmbedServer class, call its start method, and look at the source code of the start method:

   public void start(final String address, final int port, final String appname, final String accessToken) {
    
    
        //创建执行器处理具体调用的类
        executorBiz = new ExecutorBizImpl();
        //创建一个守护线程
        thread = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                // param
                //bossGroup线程组用于监听客户端的连接
                EventLoopGroup bossGroup = new NioEventLoopGroup();
                //workerGroup线程组用于处理连接,读写事件
                EventLoopGroup workerGroup = new NioEventLoopGroup();
                //创建线程池处理netty服务的调用
                ThreadPoolExecutor bizThreadPool = new ThreadPoolExecutor(
                        0,
                        200,
                        60L,
                        TimeUnit.SECONDS,
                        new LinkedBlockingQueue<Runnable>(2000),
                        new ThreadFactory() {
    
    
                            @Override
                            public Thread newThread(Runnable r) {
    
    
                                return new Thread(r, "xxl-job, EmbedServer bizThreadPool-" + r.hashCode());
                            }
                        },
                        new RejectedExecutionHandler() {
    
    
                            @Override
                            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    
    
                                throw new RuntimeException("xxl-job, EmbedServer bizThreadPool is EXHAUSTED!");
                            }
                        });
                try {
    
    
                    // start server
                    //创建netty
                    ServerBootstrap bootstrap = new ServerBootstrap();
                    //设置netty属性
                    bootstrap.group(bossGroup, workerGroup)
                            .channel(NioServerSocketChannel.class)   //使用非阻塞的服务端信道类型
                            .childHandler(new ChannelInitializer<SocketChannel>() {
    
    
                                @Override
                                public void initChannel(SocketChannel channel) throws Exception {
    
     //处理连接、读写事件的处理类
                                    channel.pipeline()//使用addLast向netty的channel信道中注册handler
                                            .addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS))  // beat 3N, close if idle 读空闲时长、写空闲时长、读写空闲时长、单位
                                            .addLast(new HttpServerCodec())//服务器的编解码器遵从http协议,HttpServerCodec类已经包含了HttpRequestDecoder(解码器), HttpResponseEncoder(编码器)
                                            .addLast(new HttpObjectAggregator(5 * 1024 * 1024))  // merge request & reponse to FULL ;netty提供的http消息聚合器,通过它可以把HttpMessage和HttpContent聚合成一个完整的FullHttpRequest或FullHttpResponse
                                            .addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));//自定义处理器,当监听的端口被调用时,使用自定义处理器进行具体的实现
                                }
                            })
                            .childOption(ChannelOption.SO_KEEPALIVE, true);//启用心跳保活机制,Tcp会监控连接是否有效,当连接处于空闲状态,超过了2个小时,本地的tcp会发送一个数据包给远程的Socket,如果远程没有响应,则Tcp会持续尝试11分钟,直到响应为止,若是12分钟还是没有响应,则tcp会尝试关闭此Socket连接

                    // bind
                    //绑定监听的信道端口
                    ChannelFuture future = bootstrap.bind(port).sync();

                    logger.info(">>>>>>>>>>> xxl-job remoting server start success, nettype = {}, port = {}", EmbedServer.class, port);

                    // start registry
                    //把当前执行器注册到调度中心中
                    startRegistry(appname, address);

                    // wait util stop
                    //防止代码运行结束调用finally中定义的关闭netty的方法,一直阻塞着,防止进程结束
                    future.channel().closeFuture().sync();

                } catch (InterruptedException e) {
    
    
                    logger.info(">>>>>>>>>>> xxl-job remoting server stop.");
                } catch (Exception e) {
    
    
                    logger.error(">>>>>>>>>>> xxl-job remoting server error.", e);
                } finally {
    
    
                    // stop
                    try {
    
    
                        //关闭netty的线程组
                        workerGroup.shutdownGracefully();
                        bossGroup.shutdownGracefully();
                    } catch (Exception e) {
    
    
                        logger.error(e.getMessage(), e);
                    }
                }
            }
        });
        //设置为守护线程,用户线程结束-》守护线程结束-》jvm结束
        thread.setDaemon(true);    // daemon, service jvm, user thread leave >>> daemon leave >>> jvm leave
        //启动线程
        thread.start();
    }

Create executorBiz in the start method. When there is scheduling, this class will implement specific tasks; create a daemon thread, and create a bizThreadPool thread pool in the thread's run method, which is used to handle scheduling tasks; create netty services , and bind the port number that the netty service listens to, create the EmbedHttpServerHandler class to handle the processing that netty is called, and bind HttpObjectAggregator to use FullHttpRequest to receive parameters. Look at the source code of the netty core processing class EmbedHttpServerHandler:

  public static class EmbedHttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    
    
        private static final Logger logger = LoggerFactory.getLogger(EmbedHttpServerHandler.class);

        private ExecutorBiz executorBiz;         //处理netty调用的实现类
        private String accessToken;              //token值
        private ThreadPoolExecutor bizThreadPool;//线程池

        public EmbedHttpServerHandler(ExecutorBiz executorBiz, String accessToken, ThreadPoolExecutor bizThreadPool) {
    
    
            this.executorBiz = executorBiz;
            this.accessToken = accessToken;
            this.bizThreadPool = bizThreadPool;
        }

        //继承了SimpleChannelInboundHandler,则重写他的channelRead0方法,当netty监听的端口被调用时,会调用到自定义处理类的channelRead0方法
        @Override
        protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
    
    
            // request parse
            //final byte[] requestBytes = ByteBufUtil.getBytes(msg.content());    // byteBuf.toString(io.netty.util.CharsetUtil.UTF_8);
            //获取请求的参数信息
            String requestData = msg.content().toString(CharsetUtil.UTF_8);
            //获取请求的结尾地址
            String uri = msg.uri();
            //请求方式
            HttpMethod httpMethod = msg.method();
            //复用tcp连接
            boolean keepAlive = HttpUtil.isKeepAlive(msg);
            //从请求头中根据key获取token信息
            String accessTokenReq = msg.headers().get(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN);

            // invoke
            //使用线程池执行此任务
            bizThreadPool.execute(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    // do invoke
                    //执行请求处理
                    Object responseObj = process(httpMethod, uri, requestData, accessTokenReq);

                    // to json
                    //执行结果转成json格式字符串
                    String responseJson = GsonTool.toJson(responseObj);

                    // write response
                    //把执行结果向调用端响应
                    writeResponse(ctx, keepAlive, responseJson);
                }
            });
        }

        //执行请求处理
        private Object process(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) {
    
    
            // valid
            //只支持post方式
            if (HttpMethod.POST != httpMethod) {
    
    
                return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support.");
            }
            //结尾地址为空
            if (uri == null || uri.trim().length() == 0) {
    
    
                return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty.");
            }
            //比对请求方传递的token值是否正确
            if (accessToken != null
                    && accessToken.trim().length() > 0
                    && !accessToken.equals(accessTokenReq)) {
    
    
                return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong.");
            }

            // services mapping
            try {
    
    
                //根据请求的结尾地址,调用对应的方法进行处理
                switch (uri) {
    
    
                    case "/beat":
                        //调度中心进行心跳检测
                        return executorBiz.beat();
                    case "/idleBeat":
                        //调度中心检测执行器是否忙碌
                        IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class);
                        return executorBiz.idleBeat(idleBeatParam);
                    case "/run":
                        //调度中心调度执行器执行任务
                        TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
                        return executorBiz.run(triggerParam);
                    case "/kill":
                        //调度中心调度执行器停止任务处理
                        KillParam killParam = GsonTool.fromJson(requestData, KillParam.class);
                        return executorBiz.kill(killParam);
                    case "/log":
                        //调度中心查询执行日志信息
                        LogParam logParam = GsonTool.fromJson(requestData, LogParam.class);
                        return executorBiz.log(logParam);
                    default:
                        return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping(" + uri + ") not found.");
                }
            } catch (Exception e) {
    
    
                logger.error(e.getMessage(), e);
                return new ReturnT<String>(ReturnT.FAIL_CODE, "request error:" + ThrowableUtil.toString(e));
            }
        }

        /**
         * write response
         */
        private void writeResponse(ChannelHandlerContext ctx, boolean keepAlive, String responseJson) {
    
    
            // write response
            //响应的结果值
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(responseJson, CharsetUtil.UTF_8));   //  Unpooled.wrappedBuffer(responseJson)
            //设置响应头部格式
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");       // HttpHeaderValues.TEXT_PLAIN.toString()
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
            if (keepAlive) {
    
    
                response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
            }
            //使用信道的上下文向请求方写入、刷洗响应信息
            ctx.writeAndFlush(response);
        }

        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    
    
            ctx.flush();
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    
    
            logger.error(">>>>>>>>>>> xxl-job provider netty_http server caught exception", cause);
            ctx.close();
        }

        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    
    
            if (evt instanceof IdleStateEvent) {
    
    
                ctx.channel().close();      // beat 3N, close if idle
                logger.debug(">>>>>>>>>>> xxl-job provider netty_http server close an idle channel.");
            } else {
    
    
                super.userEventTriggered(ctx, evt);
            }
        }
    }

The EmbedHttpServerHandler class is netty's custom processing class. Because netty's channel is bound to new HttpObjectAggregator, FullHttpRequest is used to receive parameters; if it inherits SimpleChannelInboundHandler, its channelRead0 method is rewritten. When the port monitored by netty is called, it will be called The channelRead0 method of the custom processing class; use the bizThreadPool thread pool to process this request, use FullHttpRequest to obtain the request parameters, uri, token and other information, verify the token value, match the uri address, and respond to each uri by executorBiz to process, write the response result to the requester using the context of the channel, and scrub the response information.

9. Initialize the daemon thread that registers the online status of the executor

​ After the initialization of netty is completed, there is a line of code that calls the executor to register to the dispatch center: startRegistry(appname, address), this method will create a daemon thread, see the source code of startRegistry:

    public void startRegistry(final String appname, final String address) {
    
    
        // start registry
        //把当前执行器注册到调度中心中
        ExecutorRegistryThread.getInstance().start(appname, address);
    }

The specific class that performs registration is ExecutorRegistryThread, see its start source code:

    public void start(final String appname, final String address){
    
    

        // valid
        //校验执行器名称不能为空
        if (appname==null || appname.trim().length()==0) {
    
    
            logger.warn(">>>>>>>>>>> xxl-job, executor registry config fail, appname is null.");
            return;
        }
        //校验调用调度中心的客户端不能为空
        if (XxlJobExecutor.getAdminBizList() == null) {
    
    
            logger.warn(">>>>>>>>>>> xxl-job, executor registry config fail, adminAddresses is null.");
            return;
        }

        //创建注册线程
        registryThread = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    

                // registry
                //当停止的之后,才跳出while循环
                while (!toStop) {
    
    
                    try {
    
    
                        //构造注册请求参数
                        RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
                        //遍历所有的调用调度中心的客户端,向所有的调度中心注册上此执行器
                        for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
    
    
                            try {
    
    
                                //使用具体的实现类AdminBizClient调用注册方法
                                ReturnT<String> registryResult = adminBiz.registry(registryParam);
                                //执行器调用调度中心的注册方法成功
                                if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
    
    
                                    registryResult = ReturnT.SUCCESS;
                                    logger.debug(">>>>>>>>>>> xxl-job registry success, registryParam:{}, registryResult:{}", new Object[]{
    
    registryParam, registryResult});
                                    break;
                                } else {
    
    
                                    logger.info(">>>>>>>>>>> xxl-job registry fail, registryParam:{}, registryResult:{}", new Object[]{
    
    registryParam, registryResult});
                                }
                            } catch (Exception e) {
    
    
                                logger.info(">>>>>>>>>>> xxl-job registry error, registryParam:{}", registryParam, e);
                            }

                        }
                    } catch (Exception e) {
    
    
                        if (!toStop) {
    
    
                            logger.error(e.getMessage(), e);
                        }

                    }

                    try {
    
    
                        if (!toStop) {
    
    
                            //默认休眠30秒,继续向调度中心中注册当前执行器在线的信息,心跳的方式
                            TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
                        }
                    } catch (InterruptedException e) {
    
    
                        if (!toStop) {
    
    
                            logger.warn(">>>>>>>>>>> xxl-job, executor registry thread interrupted, error msg:{}", e.getMessage());
                        }
                    }
                }

                // registry remove
                //删除执行器注册信息,当程序停止或者调用了stop方法之后,会跳出上面的while循环
                try {
    
    
                    //构造删除注册请求参数
                    RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
                    //遍历所有的调用调度中心的客户端,向所有的调度中心删除此执行器
                    for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
    
    
                        try {
    
    
                            //使用具体的实现类AdminBizClient调用删除执行器的方法
                            ReturnT<String> registryResult = adminBiz.registryRemove(registryParam);
                            if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
    
    
                                registryResult = ReturnT.SUCCESS;
                                logger.info(">>>>>>>>>>> xxl-job registry-remove success, registryParam:{}, registryResult:{}", new Object[]{
    
    registryParam, registryResult});
                                break;
                            } else {
    
    
                                logger.info(">>>>>>>>>>> xxl-job registry-remove fail, registryParam:{}, registryResult:{}", new Object[]{
    
    registryParam, registryResult});
                            }
                        } catch (Exception e) {
    
    
                            if (!toStop) {
    
    
                                logger.info(">>>>>>>>>>> xxl-job registry-remove error, registryParam:{}", registryParam, e);
                            }

                        }

                    }
                } catch (Exception e) {
    
    
                    if (!toStop) {
    
    
                        logger.error(e.getMessage(), e);
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, executor registry thread destroy.");

            }
        });
        //设置为守护线程
        registryThread.setDaemon(true);
        registryThread.setName("xxl-job, executor ExecutorRegistryThread");
        //启动线程
        registryThread.start();
    }

The registryThread registration daemon thread sleeps for 30 seconds by default, executes the registration method to the dispatch center in a loop, and tells the dispatch center that it is still online. When the thread stops, that is, when toStop is true, a request to remove the registration is sent to the dispatch center, that is, it tells the dispatch center that it is offline.

10. Initial resource summary diagram

insert image description here

2. Proactively initiate a request

​ The executor needs to interact with the scheduling center. The method contained in the executor's initiative to initiate a request can be seen from its client class AdminBizClient. This type exists in the public core xxl-job-core project of xxl-job, and the directory structure is com .xxl.job.core.biz.client, you can see from the class that it contains three methods: callback, registry, and registryRemove. Look at the source code of AdminBizClient:

/**
 * admin api test
 * 执行器-》调用调度中心的客户端,供执行器使用
 * @author xuxueli 2017-07-28 22:14:52
 */
public class AdminBizClient implements AdminBiz {
    
    

    public AdminBizClient() {
    
    
    }
    public AdminBizClient(String addressUrl, String accessToken) {
    
    
        this.addressUrl = addressUrl;
        this.accessToken = accessToken;

        // valid
        if (!this.addressUrl.endsWith("/")) {
    
    
            this.addressUrl = this.addressUrl + "/";
        }
    }

    private String addressUrl ;
    private String accessToken;
    private int timeout = 3;


    //此方法为执行器-》调度中心的反馈方法
    @Override
    public ReturnT<String> callback(List<HandleCallbackParam> callbackParamList) {
    
    
        //发起http远程调用
        return XxlJobRemotingUtil.postBody(addressUrl+"api/callback", accessToken, timeout, callbackParamList, String.class);
    }

    //此方法为执行器-》调度中心的注册方法,把当前执行器注册到调度中心中
    @Override
    public ReturnT<String> registry(RegistryParam registryParam) {
    
    
         //发起http远程调用
        return XxlJobRemotingUtil.postBody(addressUrl + "api/registry", accessToken, timeout, registryParam, String.class);
    }

    //此方法为执行器-》调度中心的删除执行器方法,把当前执行器从调度中心注册列表中删除
    @Override
    public ReturnT<String> registryRemove(RegistryParam registryParam) {
    
    
        //发起http远程调用
        return XxlJobRemotingUtil.postBody(addressUrl + "api/registryRemove", accessToken, timeout, registryParam, String.class);
    }

}

This client class provides 3 methods for calling the dispatch center. When the method is called, the interface address of the remote dispatch center will be concatenated to make a remote call. There are as many calling client classes as there are dispatching centers. When the program is initialized, these dispatching center client classes have been initialized and placed in the list collection. When it is necessary to call, directly traverse these client collections. Each client executes the calling method. Sample source code for getting a collection of calling clients and traversing calls:

     for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
    
    
                        try {
    
    
                            //使用具体的实现类AdminBizClient调用删除执行器的方法
                            ReturnT<String> registryResult = adminBiz.registryRemove(registryParam);
                            if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
    
    
                                registryResult = ReturnT.SUCCESS;
                                logger.info(">>>>>>>>>>> xxl-job registry-remove success, registryParam:{}, registryResult:{}", new Object[]{
    
    registryParam, registryResult});
                                break;
                            } else {
    
    
                                logger.info(">>>>>>>>>>> xxl-job registry-remove fail, registryParam:{}, registryResult:{}", new Object[]{
    
    registryParam, registryResult});
                            }
                        } catch (Exception e) {
    
    
                            if (!toStop) {
    
    
                                logger.info(">>>>>>>>>>> xxl-job registry-remove error, registryParam:{}", registryParam, e);
                            }

                        }

   }

    //获取调用客户端
    public static List<AdminBiz> getAdminBizList(){
    
    
        return adminBizList;
    }

1. callback call

​The callback call is for the executor to feed back the execution result value of the task to the scheduling center. The calling location in the source code is: when the feedback daemon thread inquires that there is a value in the feedback queue or when the feedback fails to retry the daemon thread detects that there is a record of feedback failure , the feedback call initiated. Look at the source code of the feedback call entry:

 while(!toStop){
    
    
                    try {
    
    
                        //使用take方法出队,take和put方法不互斥,读写分离,分别使用takeLock/putLock进行加锁
                        HandleCallbackParam callback = getInstance().callBackQueue.take();
                        //回调参数类不为空,则处理回调
                        if (callback != null) {
    
    

                            // callback list param
                            //定义一个集合接收callBackQueue队列中的所有回调类
                            List<HandleCallbackParam> callbackParamList = new ArrayList<HandleCallbackParam>();
                            //drainTo方法为把callBackQueue队列中的所有值转移到新的callbackParamList集合中,经过此方法调用,此时callBackQueue为空,callbackParamList接收到队列里面的所有元素
                            int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList);
                            //一开始出队列的的对象也要加入到集合中
                            callbackParamList.add(callback);

                            // callback, will retry if error
                            if (callbackParamList!=null && callbackParamList.size()>0) {
    
    
                                //处理反馈
                                doCallback(callbackParamList);
                            }
                        }
                    } catch (Exception e) {
    
    
                        if (!toStop) {
    
    
                            logger.error(e.getMessage(), e);
                        }
                    }
                }

The daemon thread fetches values ​​from the callBackQueue feedback queue. When there is a record that needs feedback, it takes out all the values ​​in the callBackQueue queue and calls the feedback method. The method for processing feedback is doCallback. Take a look at its source code:

 private void doCallback(List<HandleCallbackParam> callbackParamList){
    
    
        boolean callbackRet = false;
        // callback, will retry if error
        //遍历调用调度中心的客户端
        for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
    
    
            try {
    
    
                //每个客户端都进行调用
                ReturnT<String> callbackResult = adminBiz.callback(callbackParamList);
                //回调成功
                if (callbackResult!=null && ReturnT.SUCCESS_CODE == callbackResult.getCode()) {
    
    
                    callbackLog(callbackParamList, "<br>----------- xxl-job job callback finish.");
                    callbackRet = true;
                    //有一个调度中心执行成功了,则退出反馈调用,只用调用到一个就行
                    break;
                } else {
    
    
                    //回调失败
                    callbackLog(callbackParamList, "<br>----------- xxl-job job callback fail, callbackResult:" + callbackResult);
                }
            } catch (Exception e) {
    
    
                //回调错误
                callbackLog(callbackParamList, "<br>----------- xxl-job job callback error, errorMsg:" + e.getMessage());
            }
        }
        //回调有失败的情况
        if (!callbackRet) {
    
    
            //把这些失败回调都追加到回调失败日志中
            appendFailCallbackFile(callbackParamList);
        }
    }

This method traverses all the clients that call the scheduling center and executes the feedback information call. If the feedback fails, write these feedback records to the feedback failure disk directory, and write the feedback log information into the executor log file. Here is an analysis of the method callbackLog source code written to the execution log:

  private void callbackLog(List<HandleCallbackParam> callbackParamList, String logContent){
    
    
        for (HandleCallbackParam callbackParam: callbackParamList) {
    
    
            //根据日期、日志id创建日志文件的存放目录(使用日期格式:xxxx-xx-xx),得到日志文件名logId.log
            String logFileName = XxlJobFileAppender.makeLogFileName(new Date(callbackParam.getLogDateTim()), callbackParam.getLogId());
            //使用InheritableThreadLocal记录日志文件名的线程内部变量
            XxlJobContext.setXxlJobContext(new XxlJobContext(
                    -1,
                    null,
                    logFileName,
                    -1,
                    -1));
            XxlJobHelper.log(logContent);
        }
    }

Each feedback record corresponds to a task scheduling, and the end directory and file name of the execution log can be organized by using the task scheduling time + log id. Create an XxlJobContext entity to receive the file name, this XxlJobContext class provides a setXxlJobContext method, see the source code of this method:

    //使用InheritableThreadLocal来作为线程内部变量,与ThreadLocal相比InheritableThreadLocal可以在子线程中调用到父线程的线程内部变量
    private static InheritableThreadLocal<XxlJobContext> contextHolder = new InheritableThreadLocal<XxlJobContext>(); // support for child thread of job handler)

    public static void setXxlJobContext(XxlJobContext xxlJobContext){
    
    
        contextHolder.set(xxlJobContext);
    }

    public static XxlJobContext getXxlJobContext(){
    
    
        return contextHolder.get();
    }

The incoming XxlJobContext object is decorated with InheritableThreadLocal, and this variable is set as a thread variable. After this thread processing, directly call the get method to obtain the XxlJobContext object set by the process in front of this thread, which is thread-isolated. Look at the source code of the method XxlJobHelper.log(logContent) it writes to the log:

 public static boolean log(String appendLogPattern, Object ... appendLogArguments) {
    
    
        //按格式进行占位符号的替代
        FormattingTuple ft = MessageFormatter.arrayFormat(appendLogPattern, appendLogArguments);
        //获取到日志信息
        String appendLog = ft.getMessage();

        /*appendLog = appendLogPattern;
        if (appendLogArguments!=null && appendLogArguments.length>0) {
            appendLog = MessageFormat.format(appendLogPattern, appendLogArguments);
        }*/
        //获取调用者的堆栈信息,可以获取到调用者的类名callInfo.getClassName()、方法名callInfo.getMethodName()
        StackTraceElement callInfo = new Throwable().getStackTrace()[1];
        //处理日志详情
        return logDetail(callInfo, appendLog);
    }

Replace the placeholder symbol according to the format, get the log information, get the stack information of the caller, you can get the caller's class name callInfo.getClassName(), method name callInfo.getMethodName(), you need to add the log when adding Information such as the class name called on. Look at the logDetail source code of the method for processing log details:

 private static boolean logDetail(StackTraceElement callInfo, String appendLog) {
    
    
        //从InheritableThreadLocal中获取到内部线程变量值,获取到上面设置的日志文件信息
        XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext();
        if (xxlJobContext == null) {
    
    
            return false;
        }

        /*// "yyyy-MM-dd HH:mm:ss [ClassName]-[MethodName]-[LineNumber]-[ThreadName] log";
        StackTraceElement[] stackTraceElements = new Throwable().getStackTrace();
        StackTraceElement callInfo = stackTraceElements[1];*/
        //组织日志信息
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(DateUtil.formatDateTime(new Date())).append(" ")
                .append("["+ callInfo.getClassName() + "#" + callInfo.getMethodName() +"]").append("-")
                .append("["+ callInfo.getLineNumber() +"]").append("-")
                .append("["+ Thread.currentThread().getName() +"]").append(" ")
                .append(appendLog!=null?appendLog:"");
        String formatAppendLog = stringBuffer.toString();

        // appendlog
        //获取日志文件名称
        String logFileName = xxlJobContext.getJobLogFileName();

        if (logFileName!=null && logFileName.trim().length()>0) {
    
    
            //把日志信息追加到某个日志文件名下
            XxlJobFileAppender.appendLog(logFileName, formatAppendLog);
            return true;
        } else {
    
    
            logger.info(">>>>>>>>>>> {}", formatAppendLog);
            return false;
        }
    }

Through XxlJobContext.getXxlJobContext(), you can get the previously set thread variable, get the file name of the log from this variable, organize the log information, and append the log information to the log file. Look at the appendLog source code of the method of appending logs:

	public static void appendLog(String logFileName, String appendLog) {
    
    

		// log file
		if (logFileName==null || logFileName.trim().length()==0) {
    
    
			return;
		}
		File logFile = new File(logFileName);
		//日志文件xx.log不存在,则进行创建
		if (!logFile.exists()) {
    
    
			try {
    
    
				//创建文件
				logFile.createNewFile();
			} catch (IOException e) {
    
    
				logger.error(e.getMessage(), e);
				return;
			}
		}

		// log
		if (appendLog == null) {
    
    
			appendLog = "";
		}
		appendLog += "\r\n";
		
		// append file content
		//把日志信息追加到日志文件中
		FileOutputStream fos = null;
		try {
    
    
			fos = new FileOutputStream(logFile, true);
			fos.write(appendLog.getBytes("utf-8"));
			fos.flush();
		} catch (Exception e) {
    
    
			logger.error(e.getMessage(), e);
		} finally {
    
    
			if (fos != null) {
    
    
				try {
    
    
					fos.close();
				} catch (IOException e) {
    
    
					logger.error(e.getMessage(), e);
				}
			}
		}
	}

​ When the feedback fails, the failure information needs to be written into the directory file storing the feedback failure, see the source code appendFailCallbackFile for handling the feedback failure:

    //把这些失败回调都追加到反馈失败日志中
    private void appendFailCallbackFile(List<HandleCallbackParam> callbackParamList){
    
    
        // valid
        if (callbackParamList==null || callbackParamList.size()==0) {
    
    
            return;
        }

        // append file
        //将对象转成byte数组
        byte[] callbackParamList_bytes = JdkSerializeTool.serialize(callbackParamList);

        //创建反馈错误日志文件-以时间为名称
        File callbackLogFile = new File(failCallbackFileName.replace("{x}", String.valueOf(System.currentTimeMillis())));
        //若是此文件已经存在
        if (callbackLogFile.exists()) {
    
    
            /*for (int i = 0; i < 100; i++) {
                callbackLogFile = new File(failCallbackFileName.replace("{x}", String.valueOf(System.currentTimeMillis()).concat("-").concat(String.valueOf(i)) ));
                if (!callbackLogFile.exists()) {
                    break;
                }
            }*/
            //使用时间+序号的方式获取到唯一的文件名
            int fileIndex = 0;
            while(true) {
    
    
                callbackLogFile = new File(failCallbackFileName.replace("{x}", String.valueOf(System.currentTimeMillis()).concat("-").concat(String.valueOf(fileIndex++)) ));
                if (!callbackLogFile.exists()) {
    
    
                    break;
                }
            }
        }
        //把错误反馈日志文件写入到错误日志中
        FileUtil.writeFileContent(callbackLogFile, callbackParamList_bytes);
    }

Convert the collection of feedback failures into a byte array, create a feedback failure log file - name it with time, if there are repetitions at the same time, add a serial number, and write the feedback failure log information into the failure log file. In this way, when the daemon thread of the retry feedback failure record is executed next time, it can load the failure record and give retry feedback.

2. registry call

​ The registry call is for the executor to call the scheduling center to update the latest online time of the executor. After receiving the request, the scheduling center will update the update_time field of the xxl_job_registry table, so that the executor will not be deleted when the scheduling center regularly cleans up the offline executor. The calling location in the source code is:

  while (!toStop) {
    
    
                    try {
    
    
                        //构造注册请求参数
                        RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
                        //遍历所有的调用调度中心的客户端,向所有的调度中心注册上此执行器
                        for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
    
    
                            try {
    
    
                                //使用具体的实现类AdminBizClient调用注册方法
                                ReturnT<String> registryResult = adminBiz.registry(registryParam);
                                //执行器调用调度中心的注册方法成功
                                if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
    
    
                                    registryResult = ReturnT.SUCCESS;
                                    logger.debug(">>>>>>>>>>> xxl-job registry success, registryParam:{}, registryResult:{}", new Object[]{
    
    registryParam, registryResult});
                                    break;
                                } else {
    
    
                                    logger.info(">>>>>>>>>>> xxl-job registry fail, registryParam:{}, registryResult:{}", new Object[]{
    
    registryParam, registryResult});
                                }
                            } catch (Exception e) {
    
    
                                logger.info(">>>>>>>>>>> xxl-job registry error, registryParam:{}", registryParam, e);
                            }

                        }
                    } catch (Exception e) {
    
    
                        if (!toStop) {
    
    
                            logger.error(e.getMessage(), e);
                        }

                    }

                    try {
    
    
                        if (!toStop) {
    
    
                            //默认休眠30秒,继续向调度中心中注册当前执行器在线的信息,心跳的方式
                            TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
                        }
                    } catch (InterruptedException e) {
    
    
                        if (!toStop) {
    
    
                            logger.warn(">>>>>>>>>>> xxl-job, executor registry thread interrupted, error msg:{}", e.getMessage());
                        }
                    }
                }

The executor sleeps for 30 seconds by default and calls the registration method in a loop.

3. registryRemove call

​ When the executor is offline, it is necessary to notify the scheduling center to delete the registration record of the executor from the xxl_job_registry table, so that the executor can be removed in time when the daemon thread next checks the online executor of a certain task group. Where it is used in the source code:

                //删除执行器注册信息,当程序停止或者调用了stop方法之后,会跳出上面的while循环
                try {
    
    
                    //构造注册请求参数
                    RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
                    //遍历所有的调用调度中心的客户端,向所有的调度中心删除此执行器
                    for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
    
    
                        try {
    
    
                            //使用具体的实现类AdminBizClient调用删除执行器的方法
                            ReturnT<String> registryResult = adminBiz.registryRemove(registryParam);
                            if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
    
    
                                registryResult = ReturnT.SUCCESS;
                                logger.info(">>>>>>>>>>> xxl-job registry-remove success, registryParam:{}, registryResult:{}", new Object[]{
    
    registryParam, registryResult});
                                break;
                            } else {
    
    
                                logger.info(">>>>>>>>>>> xxl-job registry-remove fail, registryParam:{}, registryResult:{}", new Object[]{
    
    registryParam, registryResult});
                            }
                        } catch (Exception e) {
    
    
                            if (!toStop) {
    
    
                                logger.info(">>>>>>>>>>> xxl-job registry-remove error, registryParam:{}", registryParam, e);
                            }

                        }

                    }
                } catch (Exception e) {
    
    
                    if (!toStop) {
    
    
                        logger.error(e.getMessage(), e);
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, executor registry thread destroy.");

            }

When the daemon thread that registers the executor is stopped, it will jump out of the while loop, and then execute the method to remove the executor registration.

3. Receive request processing

​ The request of the executor to receive the call center is monitored by netty. The specific requests received can be viewed from the internal class EmbedHttpServerHandler of the EmbedServer class. This type exists in the public core xxl-job-core project of xxl-job. The directory structure is com.xxl.job.core.server, the specific request can be seen from the process method of the internal class EmbedHttpServerHandler, which includes five methods: beat, idleBeat, run, kill, and log. Look at the process method source code:

        private Object process(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) {
    
    
            // valid
            //只支持post方式
            if (HttpMethod.POST != httpMethod) {
    
    
                return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support.");
            }
            //结尾地址为空
            if (uri == null || uri.trim().length() == 0) {
    
    
                return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty.");
            }
            //比对请求方传递的token值是否正确
            if (accessToken != null
                    && accessToken.trim().length() > 0
                    && !accessToken.equals(accessTokenReq)) {
    
    
                return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong.");
            }

            // services mapping
            try {
    
    
                //根据请求的结尾地址,调用对应的方法进行处理
                switch (uri) {
    
    
                    case "/beat":
                        //调度中心进行心跳检测
                        return executorBiz.beat();
                    case "/idleBeat":
                        //调度中心检测执行器是否忙碌
                        IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class);
                        return executorBiz.idleBeat(idleBeatParam);
                    case "/run":
                        //调度中心调度执行器执行任务
                        TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
                        return executorBiz.run(triggerParam);
                    case "/kill":
                        //调度中心调度执行器停止任务处理
                        KillParam killParam = GsonTool.fromJson(requestData, KillParam.class);
                        return executorBiz.kill(killParam);
                    case "/log":
                        //调度中心查询执行日志信息
                        LogParam logParam = GsonTool.fromJson(requestData, LogParam.class);
                        return executorBiz.log(logParam);
                    default:
                        return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping(" + uri + ") not found.");
                }
            } catch (Exception e) {
    
    
                logger.error(e.getMessage(), e);
                return new ReturnT<String>(ReturnT.FAIL_CODE, "request error:" + ThrowableUtil.toString(e));
            }
        }

The method only supports the post method. The specific implementation of the specific processing class executorBiz is the ExecutorBizImpl class, which has been created when the program starts and initializes.

1. beat request

​ The beat request is an interface for the dispatch center to confirm whether the executor is online. If it can be adjusted normally, it means that the executor is online. If it fails, it means that the executor is offline. It is called by the dispatch center when using the failover routing mode. Look at the beat source code:

    @Override
    public ReturnT<String> beat() {
    
    
        return ReturnT.SUCCESS;
    }

It returns success directly, and it is success if it can be adjusted.

2. idle Beat request

​ The idleBeat request is an interface for the scheduling center to confirm whether the executor is busy. When the executor is still processing the last scheduling of this task, this executor will not be selected for this scheduling. This is when the scheduling center uses the busy transfer routing mode. transfer. Look at the idleBeat source code:

    //响应调度中心确认执行器是否忙碌
    @Override
    public ReturnT<String> idleBeat(IdleBeatParam idleBeatParam) {
    
    

        // isRunningOrHasQueue
        boolean isRunningOrHasQueue = false;
        //根据任务id获取处理此任务的线程类
        JobThread jobThread = XxlJobExecutor.loadJobThread(idleBeatParam.getJobId());
        //线程类存在,且正在运行或者还有未处理完的任务队列
        if (jobThread != null && jobThread.isRunningOrHasQueue()) {
    
    
            //标记为true
            isRunningOrHasQueue = true;
        }
        //为true,表示此执行器现在正在处理这个任务的上一次调度
        if (isRunningOrHasQueue) {
    
    
            return new ReturnT<String>(ReturnT.FAIL_CODE, "job thread is running or has trigger queue.");
        }
        return ReturnT.SUCCESS;
    }

Determine whether there is a thread executing this task, if yes, return busy, if not, return success. Obtain the corresponding thread class and whether it is busy according to the task id and put it in the run request.

3. run request

​ The run request is the interface for the executor to respond to the running tasks of the scheduling center and execute specific tasks. Look at the run source code:

    //响应调度中心执行任务
    @Override
    public ReturnT<String> run(TriggerParam triggerParam) {
    
    
        // load old:jobHandler + jobThread
        //根据任务id获取任务线程类,从jobThreadRepository中获取,key:任务id,value:任务线程类
        JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId());
        //从任务线程类获取绑定的任务处理器
        IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null;
        String removeOldReason = null;

        // valid:jobHandler + jobThread
        //获取任务的运行模式
        GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType());
        //bean模式
        if (GlueTypeEnum.BEAN == glueTypeEnum) {
    
    

            // new jobhandler
            //获取执行器任务handler:使用@XxlJob修饰的值,从集合jobHandlerRepository中获取,key:@XxlJob注解的value值,value:此任务执行的对象,包含Bean对象,执行的方法、初始方法、销毁方法
            //程序启动的时候,所有被@XxlJob修饰的处理类都添加到jobHandlerRepository集合中了
            IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler(triggerParam.getExecutorHandler());

            // valid old jobThread
            //上一次此任务id绑定的任务处理器不等于此次执行的任务处理器
            if (jobThread!=null && jobHandler != newJobHandler) {
    
    
                // change handler, need kill old thread
                removeOldReason = "change jobhandler or glue type, and terminate the old job thread.";
                //线程设置为null
                jobThread = null;
                //线程绑定的处理器也设置为null
                jobHandler = null;
            }

            // valid handler
            //给任务处理器重新赋值
            if (jobHandler == null) {
    
    
                jobHandler = newJobHandler;
                if (jobHandler == null) {
    
    
                    return new ReturnT<String>(ReturnT.FAIL_CODE, "job handler [" + triggerParam.getExecutorHandler() + "] not found.");
                }
            }

        } else if (GlueTypeEnum.GLUE_GROOVY == glueTypeEnum) {
    
    

            // valid old jobThread
            if (jobThread != null &&
                    !(jobThread.getHandler() instanceof GlueJobHandler
                        && ((GlueJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
    
    
                // change handler or gluesource updated, need kill old thread
                removeOldReason = "change job source or glue type, and terminate the old job thread.";

                jobThread = null;
                jobHandler = null;
            }

            // valid handler
            if (jobHandler == null) {
    
    
                try {
    
    
                    IJobHandler originJobHandler = GlueFactory.getInstance().loadNewInstance(triggerParam.getGlueSource());
                    jobHandler = new GlueJobHandler(originJobHandler, triggerParam.getGlueUpdatetime());
                } catch (Exception e) {
    
    
                    logger.error(e.getMessage(), e);
                    return new ReturnT<String>(ReturnT.FAIL_CODE, e.getMessage());
                }
            }
        } else if (glueTypeEnum!=null && glueTypeEnum.isScript()) {
    
    

            // valid old jobThread
            if (jobThread != null &&
                    !(jobThread.getHandler() instanceof ScriptJobHandler
                            && ((ScriptJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
    
    
                // change script or gluesource updated, need kill old thread
                removeOldReason = "change job source or glue type, and terminate the old job thread.";

                jobThread = null;
                jobHandler = null;
            }

            // valid handler
            if (jobHandler == null) {
    
    
                jobHandler = new ScriptJobHandler(triggerParam.getJobId(), triggerParam.getGlueUpdatetime(), triggerParam.getGlueSource(), GlueTypeEnum.match(triggerParam.getGlueType()));
            }
        } else {
    
    
            return new ReturnT<String>(ReturnT.FAIL_CODE, "glueType[" + triggerParam.getGlueType() + "] is not valid.");
        }

        // executor block strategy
        //任务id对应的线程不为空
        if (jobThread != null) {
    
    
            //获取阻塞处理策略
            ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerParam.getExecutorBlockStrategy(), null);
            //丢弃后续调度
            if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) {
    
    
                // discard when running
                //线程正在运行或队列里面还有任务,则丢弃此次任务调度
                if (jobThread.isRunningOrHasQueue()) {
    
    
                    return new ReturnT<String>(ReturnT.FAIL_CODE, "block strategy effect:"+ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle());
                }
            } else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) {
    
    
                //覆盖之前调度
                // kill running jobThread
                //线程正在运行或队列里面还有任务,则覆盖之前调度
                if (jobThread.isRunningOrHasQueue()) {
    
    
                    removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle();
                    //任务线程设置为null
                    jobThread = null;
                }
            } else {
    
    
                // just queue trigger
            }
        }

        // replace thread (new or exists invalid)
        //经过上面的校验处理,此任务id对应的任务线程类还是为空
        if (jobThread == null) {
    
    
            jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
        }

        // push data to queue
        //把任务放到调度队列里面
        ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);
        //返回调度结果
        return pushResult;
    }

When the executor receives a scheduling task request, it will check whether the thread processing class can be reused. The newly created thread processing class will be placed in the map collection, key: task id, value: task thread class, see Obtaining tasks according to the task id Thread class method loadJobThread source code:

    //存放任务、任务线程类集合,key:任务id,value:任务线程类
    private static ConcurrentMap<Integer, JobThread> jobThreadRepository = new ConcurrentHashMap<Integer, JobThread>();

    //根据任务id加载此任务的处理线程类
    public static JobThread loadJobThread(int jobId){
    
    
        return jobThreadRepository.get(jobId);
    }

Use ConcurrentMap to store the created thread processing class JobThread. Before using it, check whether it can be reused. If it has been created and still exists, it can be reused. The JobThread class is bound to its processing class handler. When the JobThread class can be obtained through the task id, the handler class can be obtained. The handler class is the encapsulation of the processing task method initialized by parsing the @XxlJob annotation when the program starts. Look at part of the source code of the JobThread class:

public class JobThread extends Thread{
    
    
	private static Logger logger = LoggerFactory.getLogger(JobThread.class);

	private int jobId;   //任务id
	private IJobHandler handler;//处理器
	private LinkedBlockingQueue<TriggerParam> triggerQueue; //存放执行任务的阻塞队列
	private Set<Long> triggerLogIdSet;		//去重调度日志

	private volatile boolean toStop = false;
	private String stopReason;

    private boolean running = false;    // if running job
	private int idleTimes = 0;			// 停止线程的中断标识
}

JobThread inherits Thread, and can use the characteristics of threads to run methods; it binds task ids and processors, and uses blocking queues to store pending tasks.

​ Obtain the operation mode of the task, for example, the bean mode uses @XxlJob to implement specific tasks, the shell mode is implemented in the form of an executable file, and an IJobHandler is created according to the operation mode. IJobHandler is an abstract parent class, and its subclasses contain 3. The screenshot is as follows:
insert image description here

Let's analyze the bean mode here. Other modes are similar. Let's take a look at the source code for processing the bean mode:

        if (GlueTypeEnum.BEAN == glueTypeEnum) {
    
    

            // new jobhandler
            //获取执行器任务handler:使用@XxlJob修饰的值,从集合jobHandlerRepository中获取,key:@XxlJob注解的value值,value:此任务执行的对象,包含Bean对象,执行的方法、初始方法、销毁方法
            //程序启动的时候,所有的被@XxlJob修饰的处理类都添加到jobHandlerRepository集合中了
            IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler(triggerParam.getExecutorHandler());

            // valid old jobThread
            //上一次此任务id绑定的任务处理器不等于此次执行的任务处理器
            if (jobThread!=null && jobHandler != newJobHandler) {
    
    
                // change handler, need kill old thread
                removeOldReason = "change jobhandler or glue type, and terminate the old job thread.";
                //线程设置为null
                jobThread = null;
                //线程绑定的处理器也设置为null
                jobHandler = null;
            }

            // valid handler
            //给任务处理器重新赋值
            if (jobHandler == null) {
    
    
                jobHandler = newJobHandler;
                if (jobHandler == null) {
    
    
                    return new ReturnT<String>(ReturnT.FAIL_CODE, "job handler [" + triggerParam.getExecutorHandler() + "] not found.");
                }
            }

        }

According to the jobHandler value of the task, in the processing method modified by @XxlJob initialized from the start of the program, the handler corresponding to the jobHandler is matched. See the source code for obtaining the handler:

    //job处理器集合,key:@XxlJob注解的value值,value:此任务执行的对象,包含Bean对象,执行的方法、初始方法、销毁方法    
    private static ConcurrentMap<String, IJobHandler> jobHandlerRepository = new ConcurrentHashMap<String, IJobHandler>();

    public static IJobHandler loadJobHandler(String name){
    
    
        return jobHandlerRepository.get(name);
    }

    public static IJobHandler registJobHandler(String name, IJobHandler jobHandler){
    
    
        logger.info(">>>>>>>>>>> xxl-job register jobhandler success, name:{}, jobHandler:{}", name, jobHandler);
        //把任务添加处理器集合中,后续当需要处理某个@XxlJob定义的任务时,直接从jobHandlerRepository集合用key取出,直接调用它的执行方法即可
        return jobHandlerRepository.put(name, jobHandler);
    }

Obtain the handler according to the key from the jobHandlerRepository collection.

​ When the JobThread can be reused, but the execution handler bound to the upload task is not equal to the handler this time, that is to say, the jobHandler set for this task last time is test1 and this time it is set to test2. For such a situation, a new one needs to be created The JobThread class, make the jobThread equal to null, and then create the JobThread after judging that the jobThread is null.

​ When the JobThread can be reused, it means that the last scheduling may not be processed yet, and it needs to be processed according to the configured blocking processing strategy. When the strategy is to discard the subsequent scheduling, and the task thread is running or there are unprocessed tasks in the task queue, this scheduling will not be executed, this scheduling will be discarded, and the execution of the last scheduling will be given priority; when the strategy is to override the previous scheduling, And if the task thread is running or there are unprocessed tasks in the task queue, set the jobThread to null. When the JobThread is recreated, the JobThread bound between the task ids will be interrupted, so that the scheduling before coverage can be achieved. Reflection in the source code:

        if (jobThread != null) {
    
    
            //获取阻塞处理策略
            ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerParam.getExecutorBlockStrategy(), null);
            //丢弃后续调度
            if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) {
    
    
                // discard when running
                //线程正在运行或队列里面还有任务,则丢弃此次任务调度
                if (jobThread.isRunningOrHasQueue()) {
    
    
                    return new ReturnT<String>(ReturnT.FAIL_CODE, "block strategy effect:"+ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle());
                }
            } else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) {
    
    
                //覆盖之前调度
                // kill running jobThread
                //线程正在运行或队列里面还有任务,则覆盖之前调度
                if (jobThread.isRunningOrHasQueue()) {
    
    
                    removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle();
                    //任务线程设置为null
                    jobThread = null;
                }
            } else {
    
    
                // just queue trigger
            }
        }

​ After the above verification, if the JobThread cannot be reused, a new one needs to be created. Let’s look at the source code for creating a JobThread:

        if (jobThread == null) {
    
    
            jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
        }

The creation method is in the registJobThread of the XxlJobExecutor class, see its source code:

    //存放任务、任务线程集合,key:任务id,value:任务线程类
    private static ConcurrentMap<Integer, JobThread> jobThreadRepository = new ConcurrentHashMap<Integer, JobThread>();

    //注册一个任务线程
    public static JobThread registJobThread(int jobId, IJobHandler handler, String removeOldReason){
    
    
        JobThread newJobThread = new JobThread(jobId, handler);
        //启动线程,开始运行JobThread重写的run方法
        newJobThread.start();
        logger.info(">>>>>>>>>>> xxl-job regist JobThread success, jobId:{}, handler:{}", new Object[]{
    
    jobId, handler});

        //ConcurrentMap的put方法,当key重复的时候,会返回旧的值,但是会把新的值进行覆盖;putIfAbsent是key重复,则返回旧的值,但是不进行覆盖
        JobThread oldJobThread = jobThreadRepository.put(jobId, newJobThread);	// putIfAbsent | oh my god, map's put method return the old value!!!
        //当新建的任务线程已经存在,则把原来的线程中断
        if (oldJobThread != null) {
    
    
            oldJobThread.toStop(removeOldReason);
            oldJobThread.interrupt();
        }

        return newJobThread;
    }

Create a JobThread class and bind its task id and handler. The JobThread class inherits Thread, and calling the start method will execute the run method in the JobThread class. Store the created JobThread in the map and use the put method of ConcurrentMap. When the key is repeated, the old value will be returned, but the new value will be overwritten. When there is an old value, the old thread will be interrupted, so that It meets the requirement that the blocking processing strategy is to cover the previous scheduling.

​ After processing the JobThread class, save this scheduling in its task queue. The source code is:

   ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);

Look at the pushTriggerQueue source code method:

	public ReturnT<String> pushTriggerQueue(TriggerParam triggerParam) {
    
    
		// avoid repeat
		//调度日志id检验是否重复
		if (triggerLogIdSet.contains(triggerParam.getLogId())) {
    
    
			logger.info(">>>>>>>>>>> repeate trigger job, logId:{}", triggerParam.getLogId());
			return new ReturnT<String>(ReturnT.FAIL_CODE, "repeate trigger job, logId:" + triggerParam.getLogId());
		}
        //日志id添加到集合中
		triggerLogIdSet.add(triggerParam.getLogId());
		//调度参数实体添加到调度队列中
		triggerQueue.add(triggerParam);
        return ReturnT.SUCCESS;
	}

The task is stored in the task queue triggerQueue, and the run request is processed this time, and the scheduling result can be fed back to the executor.

​ Because JobThread calls the start method, it will execute its run method, see the source code of the run method:

    //线程调用start()方法后,会执行run方法
    @Override
	public void run() {
    
    

    	// init
    	try {
    
    
    		//先执行初始化方法
			handler.init();
		} catch (Throwable e) {
    
    
    		logger.error(e.getMessage(), e);
		}

		// execute
		//不停止线程则一直执行
		while(!toStop){
    
    
			//任务运行状态设置为false
			running = false;
			//次数加1
			idleTimes++;

            TriggerParam triggerParam = null;
            try {
    
    
				// to check toStop signal, we need cycle, so wo cannot use queue.take(), instand of poll(timeout)
				//从阻塞队列里面移除队首元素,若是当前队列没有元素,则进行等待,等待时间为3秒
				triggerParam = triggerQueue.poll(3L, TimeUnit.SECONDS);
				//获取到元素
				if (triggerParam!=null) {
    
    
					//标记任务为运行状态true
					running = true;
					//重置次数
					idleTimes = 0;
					//set集合中移除这个日志id,用于去重判断
					triggerLogIdSet.remove(triggerParam.getLogId());

					// log filename, like "logPath/yyyy-MM-dd/9999.log"
					//创建执行任务的文件目录名
					String logFileName = XxlJobFileAppender.makeLogFileName(new Date(triggerParam.getLogDateTime()), triggerParam.getLogId());
					XxlJobContext xxlJobContext = new XxlJobContext(
							triggerParam.getJobId(),
							triggerParam.getExecutorParams(),
							logFileName,
							triggerParam.getBroadcastIndex(),
							triggerParam.getBroadcastTotal());

					// init job context
					//把执行任务的变量对象设置为线程内部变量,后面取参数等操作的时候可以从这这里取
					XxlJobContext.setXxlJobContext(xxlJobContext);

					// execute
					//添加日志,会从上面设置的线程内部变量xxlJobContext中取到文件名称,然后追加上日志
					XxlJobHelper.log("<br>----------- xxl-job job execute start -----------<br>----------- Param:" + xxlJobContext.getJobParam());

					//有设置任务超时时间
					if (triggerParam.getExecutorTimeout() > 0) {
    
    
						// limit timeout
						//创建一个任务线程
						Thread futureThread = null;
						try {
    
    
							//任务需要有返回值,所以使用Callable
							FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new Callable<Boolean>() {
    
    
								@Override
								public Boolean call() throws Exception {
    
    
                                    //使用子线程处理任务的时候,需要再设置一下线程变量,否则拿不到上面设置的线程变量
									// init job context
									XxlJobContext.setXxlJobContext(xxlJobContext);
                                    //执行处理器的方法,若是需要接收参数,可以使用XxlJobHelper.getJobParam方法获取,这个方法也是从线程内部变量XxlJobContext中获取的变量
									handler.execute();
									return true;
								}
							});
							futureThread = new Thread(futureTask);
							futureThread.start();
                            //在给定的时间内需要处理完成,处理不完成,抛出超时异常
							Boolean tempResult = futureTask.get(triggerParam.getExecutorTimeout(), TimeUnit.SECONDS);
						} catch (TimeoutException e) {
    
    

							XxlJobHelper.log("<br>----------- xxl-job job execute timeout");
							XxlJobHelper.log(e);

							// handle result
							//任务处理超时,给线程内部变量XxlJobContext的handleCode字段设置为502
							XxlJobHelper.handleTimeout("job execute timeout ");
						} finally {
    
    
							//中断线程
							futureThread.interrupt();
						}
					} else {
    
    
						//没有设置任务超时时间,直接调用
						// just execute
						handler.execute();
					}

					// valid execute handle data
					if (XxlJobContext.getXxlJobContext().getHandleCode() <= 0) {
    
    
						//xxlJobContext.setHandleCode为500,并把执行错信息追加到xxlJobContext.setHandleMsg
						XxlJobHelper.handleFail("job handle result lost.");
					} else {
    
    
						String tempHandleMsg = XxlJobContext.getXxlJobContext().getHandleMsg();
						tempHandleMsg = (tempHandleMsg!=null&&tempHandleMsg.length()>50000)
								?tempHandleMsg.substring(0, 50000).concat("...")
								:tempHandleMsg;
						XxlJobContext.getXxlJobContext().setHandleMsg(tempHandleMsg);
					}
					//把日志信息追加到日志文件中,使用线程内部变量从XxlJobContext中获取到当前处理任务的日志目录,往日志目录中追加日志
					XxlJobHelper.log("<br>----------- xxl-job job execute end(finish) -----------<br>----------- Result: handleCode="
							+ XxlJobContext.getXxlJobContext().getHandleCode()
							+ ", handleMsg = "
							+ XxlJobContext.getXxlJobContext().getHandleMsg()
					);

				} else {
    
    
					//次数大于30次,并且任务队列里面没有待处理的任务,则把次任务线程中断、删除
					if (idleTimes > 30) {
    
    
						if(triggerQueue.size() == 0) {
    
    	// avoid concurrent trigger causes jobId-lost
							XxlJobExecutor.removeJobThread(jobId, "excutor idel times over limit.");
						}
					}
				}
			} catch (Throwable e) {
    
    
				if (toStop) {
    
    
					//把日志信息追加到日志文件中,使用线程内部变量从XxlJobContext中获取到当前处理任务的日志目录,往日志目录中追加日志
					XxlJobHelper.log("<br>----------- JobThread toStop, stopReason:" + stopReason);
				}

				// handle result
				StringWriter stringWriter = new StringWriter();
				e.printStackTrace(new PrintWriter(stringWriter));
				String errorMsg = stringWriter.toString();
				//xxlJobContext.setHandleCode为500,并把执行错信息追加到xxlJobContext.setHandleMsg
				XxlJobHelper.handleFail(errorMsg);
				//把日志信息追加到日志文件中,使用线程内部变量从XxlJobContext中获取到当前处理任务的日志目录,往日志目录中追加日志
				XxlJobHelper.log("<br>----------- JobThread Exception:" + errorMsg + "<br>----------- xxl-job job execute end(error) -----------");
			} finally {
    
    
            	//调度参数不为空,说明进行过处理
                if(triggerParam != null) {
    
    
                    // callback handler info
					//线程没有停止
                    if (!toStop) {
    
    
                        // commonm
						//向反馈队列中添加执行结果,反馈线程会向调度中心进行反馈
                        TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
                        		triggerParam.getLogId(),
								triggerParam.getLogDateTime(),
								XxlJobContext.getXxlJobContext().getHandleCode(),
								XxlJobContext.getXxlJobContext().getHandleMsg() )
						);
                    } else {
    
    
                        // is killed
						//处理线程停止了,把反馈参数添加到反馈队列中
                        TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
                        		triggerParam.getLogId(),
								triggerParam.getLogDateTime(),
								XxlJobContext.HANDLE_CODE_FAIL,
								stopReason + " [job running, killed]" )
						);
                    }
                }
            }
        }

		// callback trigger request in queue
		//当处理线程停止,而任务队列里面还有未处理完的任务,则向调度中心反馈执行失败信息
		while(triggerQueue !=null && triggerQueue.size()>0){
    
    
			TriggerParam triggerParam = triggerQueue.poll();
			if (triggerParam!=null) {
    
    
				// is killed
				//向反馈线程的队列中加入反馈参数
				TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
						triggerParam.getLogId(),
						triggerParam.getLogDateTime(),
						XxlJobContext.HANDLE_CODE_FAIL,
						stopReason + " [job not executed, in the job queue, killed.]")
				);
			}
		}

		// destroy
		try {
    
    
			//执行销毁方法
			handler.destroy();
		} catch (Throwable e) {
    
    
			logger.error(e.getMessage(), e);
		}

		logger.info(">>>>>>>>>>> xxl-job JobThread stoped, hashCode:{}", Thread.currentThread());
	}

​ If the processing method of the task is configured with init, execute the init method; remove the first element from the blocking queue, and if there are no elements in the current queue, wait for 3 seconds, because it is the thread that starts first and then blocks Tasks stored in the queue. When the task to be processed is obtained, the task is marked as running status true, and the idleTimes is reset to 0. When the idleTimes is greater than 30 times, it means that the task to be processed has not been obtained for 30 times, and the time has been greater than 90 seconds (each task is fetched Wait up to 3 seconds, 30 times, the maximum is 90 seconds), then interrupt this thread class. Obtain the execution file address of the task, create an XxlJobContext object to receive parameters, or use its thread internal variable method to get the log file address when appending to subsequent logs.

​ When the execution timeout is set for the task, use FutureTask to create a task, then create an internal thread, and add XxlJobContext to the sub-thread. This is why InheritableThreadLocal is used to modify XxlJobContext instead of ThreadLocal. InheritableThreadLocal can be used in sub-threads The internal variables set by the parent thread are called in the thread, and ThreadLocal can only share internal variables within one thread. Use the FutureTask.get method to set the processing to be completed within a given time, and if the processing is not completed, a timeout exception will be thrown. If no timeout is set, just make a normal call and write the execution result to the execution log file.

​ When the JobThread finishes executing the task, there are no more tasks to be processed in the task queue, and the JobThread is destroyed after more than 30 idle runs. The source code is:

					//次数大于30次,并且任务队列里面没有待处理的任务,则把次任务线程中断、删除
					if (idleTimes > 30) {
    
    
						if(triggerQueue.size() == 0) {
    
    	// avoid concurrent trigger causes jobId-lost
							XxlJobExecutor.removeJobThread(jobId, "excutor idel times over limit.");
						}
					}

Call the method to delete JobThread, the source code is:

    //移除某个任务的处理线程,并中断此线程的执行
    public static JobThread removeJobThread(int jobId, String removeOldReason){
    
    
        JobThread oldJobThread = jobThreadRepository.remove(jobId);
        if (oldJobThread != null) {
    
    
            oldJobThread.toStop(removeOldReason);
            oldJobThread.interrupt();

            return oldJobThread;
        }
        return null;
    }

Delete this record from the map collection and interrupt the running of the thread.

​ When the task processing is completed, the feedback record needs to be stored in the feedback queue. This storage action is in finally, see the source code:

finally {
    
    
    //调度参数不为空,说明进行过处理
    if(triggerParam != null) {
    
    
        // callback handler info
        //线程没有停止
        if (!toStop) {
    
    
            // commonm
            //向反馈线程中添加执行结果,反馈线程会向调度中心进行反馈
            TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
                triggerParam.getLogId(),
                triggerParam.getLogDateTime(),
                XxlJobContext.getXxlJobContext().getHandleCode(),
                XxlJobContext.getXxlJobContext().getHandleMsg() )
                                              );
        } else {
    
    
            // is killed
            //处理线程停止了,把反馈参数添加到反馈队列中
            TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
                triggerParam.getLogId(),
                triggerParam.getLogDateTime(),
                XxlJobContext.HANDLE_CODE_FAIL,
                stopReason + " [job running, killed]" )
                                              );
        }
    }
}

When the value is obtained from the task queue here, that is, triggerParam is not equal to empty, when the thread is not stopped, add the execution result to the feedback queue, and the feedback thread will give feedback to the dispatching center. If the thread has stopped, it will send feedback to the feedback queue. Added flags for processing failures.

​ If the thread is interrupted, such as overriding the blocking strategy of previous scheduling, it will jump out of the while loop, and there are unfinished tasks in the task queue, put these tasks in the feedback queue, and mark the task execution failure, Look at the source code of the processing:

	    //当处理线程停止,而任务队列里面还有未处理完的任务,则向调度中心反馈执行失败信息
		while(triggerQueue !=null && triggerQueue.size()>0){
    
    
			TriggerParam triggerParam = triggerQueue.poll();
			if (triggerParam!=null) {
    
    
				// is killed
				//向反馈线程的队列中加入回调参数
				TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
						triggerParam.getLogId(),
						triggerParam.getLogDateTime(),
						XxlJobContext.HANDLE_CODE_FAIL,
						stopReason + " [job not executed, in the job queue, killed.]")
				);
			}
		}

​ If the task's processing method is configured with destroy, execute the destroy method. At this point, let's look at the source code of the isRunningOrHasQueue method to determine whether the JobThread class is running or has unfinished tasks:

    public boolean isRunningOrHasQueue() {
    
    
    	//线程正在运行或者调度队列里面还有未处理完的任务
        return running || triggerQueue.size()>0;
    }

When the task is running, running will be set to true, and triggerQueue is a collection of tasks in the blocking queue.

​ Here is an analysis of why the execute method of the handler can execute a specific method, first look at the source code of the MethodJobHandler class:

public class MethodJobHandler extends IJobHandler {
    
    

    private final Object target;    //Bean对象-包含XxlJob注解的对象
    private final Method method;    //执行的方法
    private Method initMethod;      //初始化方法
    private Method destroyMethod;   //销毁方法

    public MethodJobHandler(Object target, Method method, Method initMethod, Method destroyMethod) {
    
    
        this.target = target;
        this.method = method;

        this.initMethod = initMethod;
        this.destroyMethod = destroyMethod;
    }

    //执行处理的方法,被@XxlJob修饰的方法
    @Override
    public void execute() throws Exception {
    
    
        //方法中有定义参数,则执行的时候带有参数
        Class<?>[] paramTypes = method.getParameterTypes();
        if (paramTypes.length > 0) {
    
    
            method.invoke(target, new Object[paramTypes.length]);       // method-param can not be primitive-types
        } else {
    
    
            method.invoke(target);
        }
    }
}

The target field stores the Bean object, that is, the entire class with the @XxlJob modified method, and this class is registered as a Bean object. method, initMethod, and destroyMethod are all Method types, which are generated through the target bean using reflection. See the following part of the generated source code:

  //获取此Bean对象的class
  Class<?> clazz = bean.getClass();
  Method initMethod = null;

  //注解XxlJob是否有配置init属性
 if (xxlJob.init().trim().length() > 0) {
    
    
      try {
    
    
           //通过反射机制获取到init方法
          initMethod = clazz.getDeclaredMethod(xxlJob.init());
           //方法关闭安全检查
          initMethod.setAccessible(true);
      } catch (NoSuchMethodException e) {
    
    
         throw new RuntimeException("xxl-job method-jobhandler initMethod invalid, for[" + clazz + "#" + methodName + "] .");
      }
   }

Then execute the method using method.invoke reflection.

4. kill request

​ The kill request is the processing interface for the scheduling center to call the executor to stop the task. It is used to stop the tasks that the scheduling center has successfully scheduled but the executor has not yet executed. The source code location of the call:

    //响应调度中心停止执行器执行某个任务
    @Override
    public ReturnT<String> kill(KillParam killParam) {
    
    
        // kill handlerThread, and create new one
        //根据任务id获取线程
        JobThread jobThread = XxlJobExecutor.loadJobThread(killParam.getJobId());
        if (jobThread != null) {
    
    
            //执行删除线程的方法
            XxlJobExecutor.removeJobThread(killParam.getJobId(), "scheduling center kill job.");
            return ReturnT.SUCCESS;
        }

        return new ReturnT<String>(ReturnT.SUCCESS_CODE, "job thread already killed.");
    }

Get the JobThread class that handles this task according to the task id, then call the method to stop the JobThread thread class, and delete it from the map collection.

5. log request

​ The log request is the interface for the dispatch center to view the execution log of the executor. The log table xxl_job_log of the dispatch center records the address of the executor that handles this task. When the execution log needs to be viewed, this executor will be called to respond. Called source code:

    //响应调度中心获取某个任务的执行日志
    @Override
    public ReturnT<LogResult> log(LogParam logParam) {
    
    
        // log filename: logPath/yyyy-MM-dd/9999.log
        String logFileName = XxlJobFileAppender.makeLogFileName(new Date(logParam.getLogDateTim()), logParam.getLogId());
        //根据行数读取日志
        LogResult logResult = XxlJobFileAppender.readLog(logFileName, logParam.getFromLineNum());
        return new ReturnT<LogResult>(logResult);
    }

Organize the log file directory and file name according to the scheduling time and log id of the task, and read the log information by the start line.

	public static LogResult readLog(String logFileName, int fromLineNum){
    
    

		// valid log file
		if (logFileName==null || logFileName.trim().length()==0) {
    
    
            return new LogResult(fromLineNum, 0, "readLog fail, logFile not found", true);
		}
        //根据日志目录创建文件
		File logFile = new File(logFileName);

		if (!logFile.exists()) {
    
    
            return new LogResult(fromLineNum, 0, "readLog fail, logFile not exists", true);
		}

		// read file
		StringBuffer logContentBuffer = new StringBuffer();
		int toLineNum = 0;
		LineNumberReader reader = null;
		try {
    
    
            //读取文件
			reader = new LineNumberReader(new InputStreamReader(new FileInputStream(logFile), "utf-8"));
			String line = null;

			while ((line = reader.readLine())!=null) {
    
    
				toLineNum = reader.getLineNumber();		// [from, to], start as 1
                //读取的行大于起始行才作为结果
				if (toLineNum >= fromLineNum) {
    
    
                    //逐行拼接日志记录
					logContentBuffer.append(line).append("\n");
				}
			}
		} catch (IOException e) {
    
    
			logger.error(e.getMessage(), e);
		} finally {
    
    
			if (reader != null) {
    
    
				try {
    
    
					reader.close();
				} catch (IOException e) {
    
    
					logger.error(e.getMessage(), e);
				}
			}
		}

		// result
        //构造结果实体
		LogResult logResult = new LogResult(fromLineNum, toLineNum, logContentBuffer.toString(), false);
		return logResult;
	}

4. Destruction process at the end of the program

​ When the program starts, 5 daemon threads, 1 netty service, 1 map collection, 1 list collection, and 1 thread pool are initialized; the threads created when processing tasks need to be destroyed when the program ends. The entry class for resource destruction is XxlJobSpringExecutor.

1. Destroy the entry class

The XxlJobSpringExecutor class is the entry class for destruction because it implements the DisposableBean interface and overrides the destroy method. When the bean is destroyed, the destroy method will be executed, which can be used as the entry point for the destruction process. Look at the source code related to the destruction of the XxlJobSpringExecutor class:

public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, SmartInitializingSingleton, DisposableBean {
    
    

    // 实现DisposableBean接口,重写它的bean销毁方法
    @Override
    public void destroy() {
    
    
        super.destroy();
    }
}

    //注册xxlJobExecutor的bean
    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
    
    
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setAddress(address);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    }

After the XxlJobSpringExecutor class is created, it is decorated with @Bean and registered as a bean object. It also implements the DisposableBean interface, rewrites the destroy method, and finally calls the destroy method of the parent class XxlJobExecutor.

2. Resource destruction processing

​ The processing class for destroying resources is destroy of XxlJobExecutor, see its source code:

    //当bean销毁时调用此方法
    public void destroy(){
    
    
        // destroy executor-server
        //销毁netty服务,停止注册线程、向调度中心调用删除此执行器
        stopEmbedServer();

        // destroy jobThreadRepository
        //销毁任务线程
        if (jobThreadRepository.size() > 0) {
    
    
            for (Map.Entry<Integer, JobThread> item: jobThreadRepository.entrySet()) {
    
    
                JobThread oldJobThread = removeJobThread(item.getKey(), "web container destroy and kill the job.");
                // wait for job thread push result to callback queue
                if (oldJobThread != null) {
    
    
                    try {
    
    
                        oldJobThread.join();
                    } catch (InterruptedException e) {
    
    
                        logger.error(">>>>>>>>>>> xxl-job, JobThread destroy(join) error, jobId:{}", item.getKey(), e);
                    }
                }
            }
            jobThreadRepository.clear();
        }
        //销毁记录着被@XxlJob修饰的方法集合
        jobHandlerRepository.clear();


        // destroy JobLogFileCleanThread
        //销毁周期为一天的清除文件的线程
        JobLogFileCleanThread.getInstance().toStop();

        // destroy TriggerCallbackThread
        //销毁执行器执行反馈的线程、执行器执行失败反馈的线程
        TriggerCallbackThread.getInstance().toStop();

    }

The methods of destroying resources are all in destroy, and now we will introduce the destruction process one by one.

(1)stopEmbedServer()

​ Destroy the netty service, stop registering the daemon thread, and call the dispatch center to delete the executor. Look at the source code:

    //销毁netty服务
    private void stopEmbedServer() {
    
    
        // stop provider factory
        if (embedServer != null) {
    
    
            try {
    
    
                //销毁netty服务
                embedServer.stop();
            } catch (Exception e) {
    
    
                logger.error(e.getMessage(), e);
            }
        }
    }

Destroy the netty service, see its stop method source code:

    //销毁netty服务
    public void stop() throws Exception {
    
    
        // destroy server thread
        //启动时候创建的线程还存活则进行中断
        if (thread != null && thread.isAlive()) {
    
    
            //中断线程
            thread.interrupt();
        }

        // stop registry
        //停止注册
        stopRegistry();
        logger.info(">>>>>>>>>>> xxl-job remoting server destroy success.");
    }

The daemon thread created when initializing the netty service will be interrupted if it is still alive. When the daemon thread is interrupted, the netty service created through this thread will also be destroyed. The netty resource is closed in the finally method. The source code is as follows:

finally {
    
    
    // stop
    try {
    
    
         //关闭netty的线程组
         workerGroup.shutdownGracefully();
         bossGroup.shutdownGracefully();
     } catch (Exception e) {
    
    
         logger.error(e.getMessage(), e);
     }
}

Look at the stopRegistry source code of the method to stop the registration service:

    //停止注册
    public void stopRegistry() {
    
    
        // stop registry
        //调用执行器注册线程类的停止方法
        ExecutorRegistryThread.getInstance().toStop();
    }

The stop method is finally executed by toStop of ExecutorRegistryThread, see the source code:

    //执行器注册线程类的停止方法
    public void toStop() {
    
    
        //停止标识为true,则上面使用心跳注册机制的while会跳出循环,然后执行移除此执行器注册信息
        toStop = true;

        // interrupt and wait
        //中断注册线程
        if (registryThread != null) {
    
    
            registryThread.interrupt();
            try {
    
    
                registryThread.join();
            } catch (InterruptedException e) {
    
    
                logger.error(e.getMessage(), e);
            }
        }

    }

When the registration daemon thread loop condition toStop is set to true, it will jump out of the while loop, stop the automatic registration, and then execute the interface to remove the executor registration information, and finally interrupt the registration daemon thread.

(2) jobThreadRepository cleanup

​ jobThreadRepository is a map collection that stores the processing task thread class JobThread. Each JobThread is a thread class that needs to interrupt the thread and then clear the map collection.

(3)jobHandlerRepository.clear()

​ jobHandlerRepository is a map collection that stores the task processor class IJobHandler. Each IJobHandler is an executable processing class, and the map needs to be cleared.

(4)JobLogFileCleanThread.getInstance().toStop()

​ Destroy the file clearing daemon thread whose dormancy period is one day, see the source code:

    public void toStop() {
    
    
        //停止标识为true,则上面while的条件不满足,跳出循环
        toStop = true;

        if (localThread == null) {
    
    
            return;
        }

        // interrupt and wait
        //中断清除日志文件的守护线程
        localThread.interrupt();
        try {
    
    
            localThread.join();
        } catch (InterruptedException e) {
    
    
            logger.error(e.getMessage(), e);
        }
    }

(5)TriggerCallbackThread.getInstance().toStop()

​ Destroy the daemon thread that executes feedback and the daemon thread that executes failure feedback, see the source code:

   public void toStop(){
    
    
        //标识为true,则上面的while条件不符合,跳出循环,
        toStop = true;
        // stop callback, interrupt and wait
        //销毁回调线程
        if (triggerCallbackThread != null) {
    
        // support empty admin address
            triggerCallbackThread.interrupt();
            try {
    
    
                triggerCallbackThread.join();
            } catch (InterruptedException e) {
    
    
                logger.error(e.getMessage(), e);
            }
        }

        // stop retry, interrupt and wait
        //销毁重试调度反馈线程
        if (triggerRetryCallbackThread != null) {
    
    
            triggerRetryCallbackThread.interrupt();
            try {
    
    
                triggerRetryCallbackThread.join();
            } catch (InterruptedException e) {
    
    
                logger.error(e.getMessage(), e);
            }
        }

    }

When toStop is true, it will jump out of the logic of loop processing feedback messages. If there are unfeedback records in the feedback queue, the final feedback will be performed. The interrupt thread uses triggerCallbackThread.join(), which means that the thread has to wait for the end of the thread to run. The last feedback source code run before stopping the thread:

                //当停止反馈线程后,把当前callBackQueue反馈队列里面还没有反馈完的记录进行反馈
                try {
    
    
                    List<HandleCallbackParam> callbackParamList = new ArrayList<HandleCallbackParam>();
                    int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList);
                    if (callbackParamList!=null && callbackParamList.size()>0) {
    
    
                        doCallback(callbackParamList);
                    }
                } catch (Exception e) {
    
    
                    if (!toStop) {
    
    
                        logger.error(e.getMessage(), e);
                    }
                }

Redisson optimizes distributed lock problem

xxl-job uses the mysql write lock mechanism to prevent tasks from being repeatedly loaded when the cluster deploys the scheduling center. Every time a task is preloaded, a mysql connection is created and the table xxl_job_lock is locked. Only when the lock is successful The pre-reading processing of tasks can ensure that only one machine can be locked successfully in a cluster environment at a time. After the read-ahead task is loaded, it is necessary to release the lock and close the mysql connection, which wastes resources and increases the pressure on the database. For the problem of distributed locking, the mainstream Redisson distributed lock is used here for optimization. The optimization steps are as follows:

(1) Introduce dependencies

According to the specification defined by the project, the version number is defined in the parent project, so define the version number of Redisson in the pom.xml of the parent project:

<redisson.version>3.16.4</redisson.version>

​ Introduce the required Redisson and redis dependencies in the pom.xml of the scheduling center xxl-job-admin. The purpose of introducing redis is to obtain the connection information of redis:

		<!-- redis -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

		<!-- redisson -->
		<dependency>
			<groupId>org.redisson</groupId>
			<artifactId>redisson</artifactId>
			<version>${redisson.version}</version>
		</dependency>

(2) Configure redis connection information

​ Add redis connection information in the application.properties configuration file of the xxl-job-admin project:

### redis
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.timeout=3000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.min-idle=0

(3) Create a Redisson client

​ Create a package redis under the com.xxl.job.admin.core package of the xxl-job-admin project, create a new RedissonConfig configuration class, and define the Redisson client. Here, the redis single-node method is used. For other methods such as cluster and sentinel mode, please refer to the official website to create:

@Configuration
@EnableConfigurationProperties(value = RedisProperties.class)
public class RedissonConfig {
    
    

    //创建redisson客户端,此时默认使用单节点
    @Bean
    public RedissonClient redissonClient(RedisProperties redisProperties){
    
    
        Config config = new Config();
        config.useSingleServer().setAddress("redis://"+redisProperties.getHost()+":"+redisProperties.getPort());
        config.useSingleServer().setDatabase(redisProperties.getDatabase());
        config.useSingleServer().setPassword(redisProperties.getPassword());
        config.useSingleServer().setTimeout((int)redisProperties.getTimeout().getSeconds()*1000);
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }

}

(4) Original code modification

​ It is necessary to inject the Redisson client into the JobScheduleHelper class that handles pre-reading tasks. Since the Redisson client is a Bean object and JobScheduleHelper is a common class, it needs to be implemented by passing parameters when creating the JobScheduleHelper. The first class to call the JobScheduleHelper class is the XxlJobAdminConfig class, which is also a Bean object and the entry class for initializing resources. We inject the Redisson client into this class, and pass the Redisson client when executing the init initialization method go down.

XxlJobAdminConfig.java is modified as follows:

    //注入redisson客户端
    @Resource
    RedissonClient redissonClient;

    public void afterPropertiesSet() throws Exception {
    
    
        //初始化调度中心资源--添加参数
        xxlJobScheduler.init(redissonClient);
    }

The init method of the XxlJobScheduler class receives parameters and passes the Redisson client as a parameter when creating the JobScheduleHelper class. The modification of XxlJobScheduler.java is as follows:

   //初始化调度中心资源--接收参数
    public void init(RedissonClient redissonClient) throws Exception {
    
    
        //把redissonClient作为参数传递过去
        JobScheduleHelper.getInstance(redissonClient).start();
    }

   //销毁调度中心资源
    public void destroy() throws Exception {
    
    

        //停止预读线程、环形处理任务线程
        JobScheduleHelper.getInstance(null).toStop();

    }

The JobScheduleHelper class was created in a hungry way before, and now it needs to be created in a lazy way that receives parameters. The modification of JobScheduleHelper.java is as follows:

    //redisson客户端
    private RedissonClient redissonClient;
    private static volatile JobScheduleHelper instance = null;
    //接收参数式的懒汉式创建对象
    public static JobScheduleHelper getInstance(RedissonClient redissonClient){
    
    
        if(instance == null) {
    
    
            synchronized (JobScheduleHelper.class){
    
    
                if(instance == null) {
    
    
                    instance = new JobScheduleHelper(redissonClient);
                }
            }
        }
        return instance;
    }
    
    //创建对象时,注入redisson客户端
    public JobScheduleHelper(RedissonClient redissonClient){
    
    
        this.redissonClient = redissonClient;
    }

(5) Alternative locking mechanism

​ Modify the code for creating a mysql connection to obtain a redisson lock. Only after the lock is successfully added will the pre-reading process be performed, and modify the code for closing the mysql connection to close the redisson lock.

while (!scheduleThreadToStop) {
    
    
    //起始时间
    long start = System.currentTimeMillis();
    //获取redisson锁
    RLock lock = redissonClient.getLock("preReadJob");
    try {
    
    
        //尝试加锁
        boolean res = lock.tryLock(30, TimeUnit.SECONDS);
        if(res) {
    
    //获取到锁,进行处理

            long nowTime = System.currentTimeMillis();
            //处理预读流程...
        }

    } catch (Exception e) {
    
    

    } finally {
    
    
        //释放锁
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
    
    
            lock.unlock();
        }
    }

Guess you like

Origin blog.csdn.net/ZHANGLIZENG/article/details/129383898