分布式定时任务—xxl-job学习(三)——调度中心(xxl-job-admin)的启动和任务调度过程源码分析

分布式定时任务框架选型:Quartz与Xxl-Job深度对比 定时任务调度是分布式系统中的关键技术组件,其核心原理是通过时间触发机制执行预设业务逻辑。现代调度框架采用分布式锁、持久化存储等技术确保任务可靠性,在电商订单处理、数据同步等场景具有重要价值。Quartz作为经典调度框架,通过JobStore实现任务持久化,而Xxl-Job则创新性地采用中心化调度架构,提供可视化控制台和自动故障转移能力。本文通过金融、物流等行业的实战案例,详解两种框架在分布式环境下的配置要点与性能优化策略,并给出包含分片处理、幂等设计在内的生产级解决方案。 阅读详情

前言

接上一篇:分布式定时任务—xxl-job学习(二)——执行器的启动过程源码分析
更早:分布式定时任务—xxl-job学习(一):简单demo搭建

在上一篇我们深度分析了下xxl-job执行器组件的启动加载原理,本篇我们来深度挖掘一下调度中心(xxl-job-admin)的启动和调度过程。

xxl.job.version使用的依旧是2.2.1-SNAPSHOT版本

一、调度中心的启动

xxl-job-admin目录层级
通过上边xxl-job-admin的代码目录层级我们可以知道它包括:controller层支持在Web页面进行CRUD、核心类core、持久层dao以及业务实现层service。

首先我们先看com.xxl.job.admin.core.conf目录下的XxlJobAdminConfig.java

1.1 分析XxlJobAdminConfig类

@Component
public class XxlJobAdminConfig implements InitializingBean, DisposableBean {

    private static XxlJobAdminConfig adminConfig = null;
    public static XxlJobAdminConfig getAdminConfig() {
        return adminConfig;
    }


    // ---------------------- XxlJobScheduler ----------------------

    private XxlJobScheduler xxlJobScheduler;

    @Override
    public void afterPropertiesSet() throws Exception {
        adminConfig = this;

        xxlJobScheduler = new XxlJobScheduler();
        xxlJobScheduler.init();
    }

    @Override
    public void destroy() throws Exception {
        xxlJobScheduler.destroy();
    }


    // ---------------------- XxlJobScheduler ----------------------

    // conf
    @Value("${xxl.job.i18n}")
    private String i18n;

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

    @Value("${spring.mail.from}")
    private String emailFrom;

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

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

    @Value("${xxl.job.logretentiondays}")
    private int logretentiondays;

    // dao, service

    @Resource
    private XxlJobLogDao xxlJobLogDao;
    @Resource
    private XxlJobInfoDao xxlJobInfoDao;
    @Resource
    private XxlJobRegistryDao xxlJobRegistryDao;
    @Resource
    private XxlJobGroupDao xxlJobGroupDao;
    @Resource
    private XxlJobLogReportDao xxlJobLogReportDao;
    @Resource
    private JavaMailSender mailSender;
    @Resource
    private DataSource dataSource;
    @Resource
    private JobAlarmer jobAlarmer;


    public String getI18n() {
        if (!Arrays.asList("zh_CN", "zh_TC", "en").contains(i18n)) {
            return "zh_CN";
        }
        return i18n;
    }

    public String getAccessToken() {
        return accessToken;
    }

    public String getEmailFrom() {
        return emailFrom;
    }

    public int getTriggerPoolFastMax() {
        if (triggerPoolFastMax < 200) {
            return 200;
        }
        return triggerPoolFastMax;
    }

    public int getTriggerPoolSlowMax() {
        if (triggerPoolSlowMax < 100) {
            return 100;
        }
        return triggerPoolSlowMax;
    }

    public int getLogretentiondays() {
        if (logretentiondays < 7) {
            return -1;  // Limit greater than or equal to 7, otherwise close
        }
        return logretentiondays;
    }

    public XxlJobLogDao getXxlJobLogDao() {
        return xxlJobLogDao;
    }

    public XxlJobInfoDao getXxlJobInfoDao() {
        return xxlJobInfoDao;
    }

    public XxlJobRegistryDao getXxlJobRegistryDao() {
        return xxlJobRegistryDao;
    }

    public XxlJobGroupDao getXxlJobGroupDao() {
        return xxlJobGroupDao;
    }

    public XxlJobLogReportDao getXxlJobLogReportDao() {
        return xxlJobLogReportDao;
    }

    public JavaMailSender getMailSender() {
        return mailSender;
    }

    public DataSource getDataSource() {
        return dataSource;
    }

    public JobAlarmer getJobAlarmer() {
        return jobAlarmer;
    }

}
  • 配置了i18n(调度中心国际化配置)、accessToken(调度中心通讯TOKEN)、emailFrom(报警邮箱)、triggerPoolFastMax( 调度线程池最大线程配置)、triggerPoolSlowMax( 调度线程池最大线程配置)、logretentiondays(调度中心日志表数据保存天数)等参数;
  • 注入了DataSource、XxlJobLogDao、XxlJobInfoDao、XxlJobRegistryDao、XxlJobGroupDao、XxlJobLogReportDao、JavaMailSender、JobAlarmer等dao和service接口;
  • 同时因为它实现了InitializingBean, DisposableBean接口,所以在这个对象初始化的时候会调用afterPropertiesSet()方法。

注意:
在afterPropertiesSet()方法中新建了一个XxlJobScheduler对象,并调用了它的init()方法。

1.2 分析XxlJobScheduler.init()

public void init() throws Exception {
    // init i18n
    initI18n();

    // admin registry monitor run
    JobRegistryMonitorHelper.getInstance().start();

    // admin fail-monitor run
    JobFailMonitorHelper.getInstance().start();

    // admin lose-monitor run
    JobLosedMonitorHelper.getInstance().start();

    // admin trigger pool start
    JobTriggerPoolHelper.toStart();

    // admin log report start
    JobLogReportHelper.getInstance().start();

    // start-schedule
    JobScheduleHelper.getInstance().start();

    logger.info(">>>>>>>>> init xxl-job admin success.");
}

1.2.1 initI18n()

// ---------------------- I18n ----------------------

private void initI18n(){
    for (ExecutorBlockStrategyEnum item:ExecutorBlockStrategyEnum.values()) {
        item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name())));
    }
}

初始化新增任务页面中的阻塞处理策略相关的国际化信息。如果没有配置国际化语言则默认为中文。

1.2.2 JobRegistryMonitorHelper.getInstance().start()

private Thread registryThread;
private volatile boolean toStop = false;
public void start(){
	registryThread = 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)
						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)
						HashMap<String, List<String>> appAddressMap = new HashMap<String, List<String>>();
						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());
									}
									appAddressMap.put(appname, registryList);
								}
							}
						}

						// fresh group address
						for (XxlJobGroup group: groupList) {
							List<String> registryList = appAddressMap.get(group.getAppname());
							String addressListStr = null;
							if (registryList!=null && !registryList.isEmpty()) {
								Collections.sort(registryList);
								addressListStr = "";
								for (String item:registryList) {
									addressListStr += item + ",";
								}
								addressListStr = addressListStr.substring(0, addressListStr.length()-1);
							}
							group.setAddressList(addressListStr);
							XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group);
						}
					}
				} catch (Exception e) {
					if (!toStop) {
						logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e);
					}
				}
				try {
					TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
				} catch (InterruptedException e) {
					if (!toStop) {
						logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e);
					}
				}
			}
			logger.info(">>>>>>>>>>> xxl-job, job registry monitor thread stop");
		}
	});
	registryThread.setDaemon(true);
	registryThread.setName("xxl-job, admin JobRegistryMonitorHelper");
	registryThread.start();
}

分析:
该类主要是创建一个守护线程管理注册地址(每30秒执行一次)

  1. 从xxl_job_group表查询所有自动注册的执行器分组groupList;
  2. 删除过期注册地址,查询xxl_job_registry表中update_time小于当前时间前90秒的记录并做delete删除;
  3. 查询xxl_job_registry表中update_time大于当前时间前90秒的注册记录,并存入HashMap<String, List<String>> appAddressMap中,key为执行器的appname,value为执行器注册地址集合;
  4. 循环groupList,从HashMap<String, List<String>> appAddressMap中获取每个分组对应的地址集合,拼接每一个地址,以","分隔,并更新到xxl_job_group中。

1.2.3 JobFailMonitorHelper.getInstance().start()

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

		@Override
		public void run() {

			// monitor
			while (!toStop) {
				try {

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

							// lock log
							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
							if (log.getExecutorFailRetryCount() > 0) {
								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 && info.getAlarmEmail()!=null && info.getAlarmEmail().trim().length()>0) {
								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 {
                       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();
}

分析:

  • 新建一个预警守护线程(每十秒工作一次)
    TimeUnit.SECONDS.sleep(10);

  • 从xxl_job_log表查询失败日志记录id集合,每次取1000条;
    List<Long> failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findFailJobLogIds(1000);

    	<select id="findFailJobLogIds" resultType="long" >
    		SELECT id FROM `xxl_job_log`
    		WHERE !(
    			(trigger_code in (0, 200) and handle_code = 0)
    			OR
    			(handle_code = 200)
    		)
    		AND `alarm_status` = 0
    		ORDER BY id ASC
    		LIMIT #{pagesize}
    	</select>
    
  • 如果failLogIds不为空,则循环failLogIds

  • 锁住这条日志记录(其实是把这条记录的alarm_status字段从0变成了-1);

  • 根据日志记录id从数据库加载日志记录对象XxlJobLog,根据日志记录里的jobId加载任务对象XxlJobInfo;

  • 校验是否重试:如果任务设置了失败重试次数,那么进行失败重试。

    • 将任务信息、失败次数-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);
      
  • 校验预警状态:
    告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败
    如果任务配置了预警邮箱,调用所有实现JobAlarm接口的预警实现类的doAlarm(XxlJobInfo info, XxlJobLog jobLog)方法得到最终状态,并根据日志id把当前日志记录的状态从-1更新到最终状态。

1.2.3.1 分析失败重试中的JobTriggerPoolHelper.trigger()方法
public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) {
    helper.addTrigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
}
// job timeout count
private volatile long minTim = System.currentTimeMillis()/60000;     // ms > min
private volatile ConcurrentMap<Integer, AtomicInteger> jobTimeoutCountMap = new ConcurrentHashMap<>();


/**
 * add trigger
 */
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;
    AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
    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;
                    jobTimeoutCountMap.clear();
                }

                // incr timeout-count-map
                long cost = System.currentTimeMillis()-start;
                if (cost > 500) {       // ob-timeout threshold 500ms
                    AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1));
                    if (timeoutCount != null) {
                        timeoutCount.incrementAndGet();
                    }
                }

            }

        }
    });
}

分析:

  1. JobTriggerPoolHelper类中有一个ConcurrentMap<Integer, AtomicInteger> jobTimeoutCountMap变量,保存每个任务的执行次数,key为任务的jobId;
  2. 根据传入的jobId,从jobTimeoutCountMap取到对应的值jobTimeoutCount;
  3. 如果jobTimeoutCount不为空并且大于10(即该任务在一分钟内调度了10次)则使用慢线程池来执行任务,否则使用默认的快线程池;
  4. 调用XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);执行任务;
  5. check timeout-count-map;(==注意:==此处暂时没明白为什么要判断minTim != minTim_now,待补充。。。。)
  6. 计算调度任务的花费时间,如果大于500ms,则先调用jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1)),如果已经存在对应的key,则调用incrementAndGet()方法加1。

XxlJobTrigger.trigger()方法稍后再详细讲解。

1.2.3.2 分析失败预警中的XxlJobAdminConfig.getAdminConfig().getJobAlarmer().alarm(info, log)方法
1.2.3.2.1 分析JobAlarmer预警类
@Component
public class JobAlarmer implements ApplicationContextAware, InitializingBean {
    private static Logger logger = LoggerFactory.getLogger(JobAlarmer.class);

    private ApplicationContext applicationContext;
    private List<JobAlarm> jobAlarmList;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        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;
    }

}

分析:

  1. 这个类ApplicationContextAware, InitializingBean接口,重写了setApplicationContext()方法和afterPropertiesSet()方法,那么这个对象在初始化的时候会从Spring容器对象ApplicationContext中取到指定的类的list。在这里就是所有的JobAlarm,于是我们拿到了jobAlarmList。
  2. alarm(info, log)方法其实是循环jobAlarmList,执行每个JobAlarm类的doAlarm方法。
1.2.3.2.2 分析EmailJobAlarm邮件预警类
@Component
public class EmailJobAlarm implements JobAlarm {
    private static Logger logger = LoggerFactory.getLogger(EmailJobAlarm.class);

    /**
     * fail alarm
     *
     * @param jobLog
     */
    public boolean doAlarm(XxlJobInfo info, XxlJobLog jobLog){
        boolean alarmResult = true;

        // send monitor email
        if (info!=null && info.getAlarmEmail()!=null && info.getAlarmEmail().trim().length()>0) {

            // alarmContent
            String alarmContent = "Alarm Job LogId=" + jobLog.getId();
            if (jobLog.getTriggerCode() != ReturnT.SUCCESS_CODE) {
                alarmContent += "<br>TriggerMsg=<br>" + jobLog.getTriggerMsg();
            }
            if (jobLog.getHandleCode()>0 && jobLog.getHandleCode() != ReturnT.SUCCESS_CODE) {
                alarmContent += "<br>HandleCode=" + jobLog.getHandleMsg();
            }

            // email info
            XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(Integer.valueOf(info.getJobGroup()));
            String personal = I18nUtil.getString("admin_name_full");
            String title = I18nUtil.getString("jobconf_monitor");
            String content = MessageFormat.format(loadEmailJobAlarmTemplate(),
                    group!=null?group.getTitle():"null",
                    info.getId(),
                    info.getJobDesc(),
                    alarmContent);

            Set<String> emailSet = new HashSet<String>(Arrays.asList(info.getAlarmEmail().split(",")));
            for (String email: emailSet) {

                // make mail
                try {
                    MimeMessage mimeMessage = XxlJobAdminConfig.getAdminConfig().getMailSender().createMimeMessage();

                    MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
                    helper.setFrom(XxlJobAdminConfig.getAdminConfig().getEmailFrom(), personal);
                    helper.setTo(email);
                    helper.setSubject(title);
                    helper.setText(content, true);

                    XxlJobAdminConfig.getAdminConfig().getMailSender().send(mimeMessage);
                } catch (Exception e) {
                    logger.error(">>>>>>>>>>> xxl-job, job fail alarm email send error, JobLogId:{}", jobLog.getId(), e);

                    alarmResult = false;
                }

            }
        }

        return alarmResult;
    }

    /**
     * load email job alarm template
     *
     * @return
     */
    private static final String loadEmailJobAlarmTemplate(){
        String mailBodyTemplate = "<h5>" + I18nUtil.getString("jobconf_monitor_detail") + ":</span>" +
                "<table border=\"1\" cellpadding=\"3\" style=\"border-collapse:collapse; width:80%;\" >\n" +
                "   <thead style=\"font-weight: bold;color: #ffffff;background-color: #ff8c00;\" >" +
                "      <tr>\n" +
                "         <td width=\"20%\" >"+ I18nUtil.getString("jobinfo_field_jobgroup") +"</td>\n" +
                "         <td width=\"10%\" >"+ I18nUtil.getString("jobinfo_field_id") +"</td>\n" +
                "         <td width=\"20%\" >"+ I18nUtil.getString("jobinfo_field_jobdesc") +"</td>\n" +
                "         <td width=\"10%\" >"+ I18nUtil.getString("jobconf_monitor_alarm_title") +"</td>\n" +
                "         <td width=\"40%\" >"+ I18nUtil.getString("jobconf_monitor_alarm_content") +"</td>\n" +
                "      </tr>\n" +
                "   </thead>\n" +
                "   <tbody>\n" +
                "      <tr>\n" +
                "         <td>{0}</td>\n" +
                "         <td>{1}</td>\n" +
                "         <td>{2}</td>\n" +
                "         <td>"+ I18nUtil.getString("jobconf_monitor_alarm_type") +"</td>\n" +
                "         <td>{3}</td>\n" +
                "      </tr>\n" +
                "   </tbody>\n" +
                "</table>";

        return mailBodyTemplate;
    }

}

分析:

  1. 实现了JobAlarm预警接口
  2. 如果配置了报警邮件地址(多个地址使用","分隔),则发送预警邮件。

发送邮件这块实际上是使用了spring-boot-starter-mail,
其他发送邮件的各种方法可以参考本人的
SpringBoot学习历程(十一):SpringBoot2.X集成mail发送邮件

如果要实现自己业务的其他预警方法,只需要实现JobAlarm接口,重写doAlarm方法即可。(比如钉钉等)

1.2.4 JobLosedMonitorHelper.getInstance().start()

public class JobLosedMonitorHelper {
	private static Logger logger = LoggerFactory.getLogger(JobLosedMonitorHelper.class);
	
	private static JobLosedMonitorHelper instance = new JobLosedMonitorHelper();
	public static JobLosedMonitorHelper getInstance(){
		return instance;
	}

	// ---------------------- monitor ----------------------

	private Thread monitorThread;
	private volatile boolean toStop = false;
	public void start(){
		monitorThread = new Thread(new Runnable() {

			@Override
			public void run() {

				// monitor
				while (!toStop) {
					try {
						// 任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败;
						Date losedTime = DateUtil.addMinutes(new Date(), -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") );

								XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateHandleInfo(jobLog);
							}

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

                    try {
                        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();
	}

	public void toStop(){
		toStop = true;
		// interrupt and wait
		monitorThread.interrupt();
		try {
			monitorThread.join();
		} catch (InterruptedException e) {
			logger.error(e.getMessage(), e);
		}
	}

}

分析:
该类主要是任务结果丢失处理

  1. Date losedTime = DateUtil.addMinutes(new Date(), -10)取到十分钟前的时间;
  2. 从xxl_job_log表查询调度记录停留在 “运行中” 状态超过10min,且对应执行器心跳注册失败不在线的记录id集合;
	<select id="findLostJobIds" resultType="long" >
		SELECT t.id
		FROM xxl_job_log AS t
		WHERE t.trigger_code = 200
			and t.handle_code = 0
			and t.trigger_time <![CDATA[ <= ]]> #{losedTime}
			and t.executor_address not in (
				SELECT t2.registry_value
				FROM xxl_job_registry AS t2
			)
	</select>
  1. 如果id集合不为空,则循环更新xxl_job_log表对应记录,将本地调度主动标记失败。

1.2.5 JobTriggerPoolHelper.toStart()

// ---------------------- helper ----------------------

private static JobTriggerPoolHelper helper = new JobTriggerPoolHelper();

public static void toStart() {
    helper.start();
}
public static void toStop() {
    helper.stop();
}
// ---------------------- trigger pool ----------------------

// fast/slow thread pool
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());
                }
            });

    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());
                }
            });
}


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

分析:
初始化快慢两个线程池:fastTriggerPool和slowTriggerPool。当任务执行的时候默认使用快线程池fastTriggerPool;当任务执行失败并且重试次数达到阈值的时候就会使用慢线程池slowTriggerPool来执行任务

1.2.6 JobLogReportHelper.getInstance().start()

public class JobLogReportHelper {
    private static Logger logger = LoggerFactory.getLogger(JobLogReportHelper.class);

    private static JobLogReportHelper instance = new JobLogReportHelper();
    public static JobLogReportHelper getInstance(){
        return instance;
    }


    private Thread logrThread;
    private volatile boolean toStop = false;
    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);

                            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);

                            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);

                            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
                    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 {
                        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();
    }

    public void toStop(){
        toStop = true;
        // interrupt and wait
        logrThread.interrupt();
        try {
            logrThread.join();
        } catch (InterruptedException e) {
            logger.error(e.getMessage(), e);
        }
    }

}

分析: 创建一个守护线程(每一分钟工作一次)

  • 刷新前三天的日志相关报表数据:
    • 取到前N天的开始时间和结束时间;
    • 从xxl_job_log表统计相关指标;
          <select id="findLogReport" resultType="java.util.Map" >
      		SELECT
      			COUNT(handle_code) triggerDayCount,
      			SUM(CASE WHEN (trigger_code in (0, 200) and handle_code = 0) then 1 else 0 end) as triggerDayCountRunning,
      			SUM(CASE WHEN handle_code = 200 then 1 else 0 end) as triggerDayCountSuc
      		FROM xxl_job_log
      		WHERE trigger_time BETWEEN #{from} and #{to}
          </select>
      
    • 保存结果到xxl_job_log_report表,有则更新,没有则新增
  • 清理N天前的日志记录(N就是我们配置的xxl.job.logretentiondays参数,必须大于7,否则无效)
    • 删除xxl_job_log表trigger_time是N天前的数据。

1.2.7 JobScheduleHelper.getInstance().start()

public class JobScheduleHelper {
    private static Logger logger = LoggerFactory.getLogger(JobScheduleHelper.class);

    private static JobScheduleHelper instance = new JobScheduleHelper();
    public static JobScheduleHelper getInstance(){
        return instance;
    }

    public static final long PRE_READ_MS = 5000;    // pre read

    private Thread scheduleThread;
    private Thread ringThread;
    private volatile boolean scheduleThreadToStop = false;
    private volatile boolean ringThreadToStop = false;
    private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();

    public void start(){

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

                try {
                    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)
                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);

                        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();
                        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
                                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());

                                    // fresh next
                                    refreshNextValidTime(jobInfo, new Date());

                                } else if (nowTime > jobInfo.getTriggerNextTime()) {
                                    // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time

                                    // 1、trigger
                                    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
                                    if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {

                                        // 1、make ring second
                                        int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                        // 2、push time ring
                                        pushTimeRing(ringSecond, jobInfo.getId());

                                        // 3、fresh next
                                        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                    }

                                } else {
                                    // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time

                                    // 1、make ring second
                                    int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                    // 2、push time ring
                                    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
                    if (cost < 1000) {  // scan-overtime, not wait
                        try {
                            // pre-read period: success > scan each second; fail > skip this period;
                            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();


        // ring thread
        ringThread = new Thread(new Runnable() {
            @Override
            public void run() {

                // align second
                try {
                    TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis()%1000 );
                } catch (InterruptedException e) {
                    if (!ringThreadToStop) {
                        logger.error(e.getMessage(), e);
                    }
                }

                while (!ringThreadToStop) {

                    try {
                        // second data
                        List<Integer> ringItemData = new ArrayList<>();
                        int nowSecond = Calendar.getInstance().get(Calendar.SECOND);   // 避免处理耗时太长,跨过刻度,向前校验一个刻度;
                        for (int i = 0; i < 2; i++) {
                            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
                                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);
                        }
                    }

                    // next second, align second
                    try {
                        TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis()%1000);
                    } catch (InterruptedException e) {
                        if (!ringThreadToStop) {
                            logger.error(e.getMessage(), e);
                        }
                    }
                }
                logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
            }
        });
        ringThread.setDaemon(true);
        ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
        ringThread.start();
    }

    private void refreshNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws ParseException {
        Date nextValidTime = new CronExpression(jobInfo.getJobCron()).getNextValidTimeAfter(fromTime);
        if (nextValidTime != null) {
            jobInfo.setTriggerLastTime(jobInfo.getTriggerNextTime());
            jobInfo.setTriggerNextTime(nextValidTime.getTime());
        } else {
            jobInfo.setTriggerStatus(0);
            jobInfo.setTriggerLastTime(0);
            jobInfo.setTriggerNextTime(0);
        }
    }

    private void pushTimeRing(int ringSecond, int jobId){
        // push async ring
        List<Integer> ringItemData = ringData.get(ringSecond);
        if (ringItemData == null) {
            ringItemData = new ArrayList<Integer>();
            ringData.put(ringSecond, ringItemData);
        }
        ringItemData.add(jobId);

        logger.debug(">>>>>>>>>>> xxl-job, schedule push time-ring : " + ringSecond + " = " + Arrays.asList(ringItemData) );
    }

    public void toStop(){

        // 1、stop schedule
        scheduleThreadToStop = true;
        try {
            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 {
                TimeUnit.SECONDS.sleep(8);
            } catch (InterruptedException e) {
                logger.error(e.getMessage(), e);
            }
        }

        // stop ring (wait job-in-memory stop)
        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");
    }

}

分析:
创建了两个守护线程scheduleThread和ringThread

1.2.7.1 scheduleThread
  1. 根据treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20)计算预读取任务数;
  2. 执行for update语句锁定任务的获取资格;
  3. 从xxl_job_info表查询未来5秒内即将要执行的任务集合;
    	<select id="scheduleJobQuery" parameterType="java.util.HashMap" resultMap="XxlJobInfo">
    		SELECT <include refid="Base_Column_List" />
    		FROM xxl_job_info AS t
    		WHERE t.trigger_status = 1
    			and t.trigger_next_time <![CDATA[ <= ]]> #{maxNextTime}
    		ORDER BY id ASC
    		LIMIT #{pagesize}
    	</select>
    
  4. 如果当前任务的触发时间已经超时5秒以上,跳过本次执行,计算下一次的执行时间
  5. 如果当前时间大于任务的下次触发时间,则调用JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);执行调度;计算下一次的执行时间;如果任务正在运行中并且下一次执行时间在5秒内,则进行此任务数据的缓存,处理逻辑就是第6步;
  6. 其他情况下则存入Map<Integer, List<Integer>> ringData 中,key为下次执行时间数,value为待执行任务id集合;
  7. 更新任务的触发调度信息;
1.2.7.2 ringThread
  1. 避免处理耗时太长,跨过刻度,向前校验一个刻度;
  2. 从Map<Integer, List<Integer>> ringData中取两个刻度(当前秒和上一秒)的任务id集合;
  3. 循环第二步的任务id集合,调用JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);触发调度。

1.3 分析XxlJobTrigger核心类

1.3.1 trigger()方法

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]);
    }

}

分析:

  1. 根据jobId从数据库xxl_job_info表中加载任务信息XxlJobInfo;
  2. 校验方法入参executorParam,不为空则覆盖任务信息的executorParam;
  3. 校验方法入参failRetryCount,大于等于0则覆盖;
  4. 从数据库xxl_job_group表加载执行器信息,校验方法入参addressList,不为空则覆盖执行器的地址类型为手动注入1,地址列表为入参addressList;
  5. 校验方法入参executorShardingParam,
  6. 如果任务的路由策略是分片广播且执行器的注册地址列表不为空,需要根据分片参数调用本类中的processTrigger()方法。

1.3.2 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) {
            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());
}

分析:

  1. 处理方法入参:阻塞处理策略、路由策略、分片参数;
  2. 保存一条日志记录到xxl_job_log表;
  3. 初始化任务调度参数(包括执行器参数、日志记录id、任务信息和分片参数);
  4. 初始化调度地址(如果路由策略是分片广播,则从执行器地址列表中获取分片参数i对应的地址即可;如果是其他路由策略则调用对应ExecutorRouter的route方法获取最终执行器地址);
  5. 调用runExecutor(triggerParam, address)方法执行远程任务调度;
  6. 组装任务触发调度信息;
  7. 将触发调度信息更新到对应日志记录中。

1.3.3 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;
}

分析:

  1. 根据执行器地址获取对应的ExecutorBizClient(所有的执行器客户端会保存在ConcurrentMap<String, ExecutorBiz> executorBizRepository中);
  2. 调用ExecutorBizClient.run(triggerParam)方法;
    @Override
    public ReturnT<String> run(TriggerParam triggerParam) {
        return XxlJobRemotingUtil.postBody(addressUrl + "run", accessToken, timeout, triggerParam, String.class);
    }
    
    使用XxlJobRemotingUtil工具类发送了一个post请求
  3. 处理返回的结果信息;

二、彩蛋

本篇跟进了一下调度中心(xxl-job-admin)的启动过程和任务调度触发原理,下一篇我们继续跟进下调度中心的web页面端api调用源码分析。

【项目实战】分布式定时任务框架XXL-JOB核心源码分析 - 作业调度和执行 路由策略决定了组中哪个作业实例将执行该作业 阅读详情

相关推荐

西门子 G120 变频器报文

G120报文

mmxxoo_xxmmp的博客 8086

Linux环境下服务启动命令汇总

nginx、tomcat、jar、nacos、zookeeper、Influxdb、tomcat等常用服务在Linux环境下启动命令

learnworm的专栏 4956

Linux:在使用UEFI固件的计算机上内核是如何被启动的

启动计算机通常不是一件难事:按下电源键,稍等片刻,你就能看到一个登录界面,再输入正确的密码,就可以开启一天的网上冲浪之旅了。但偶尔这件事没那么顺利,有时候迎接你的不是熟悉的登录界面,而是一个令人生畏的命令提示符界面,一闪一闪的提示符告诉你:“你碰到麻烦了”。于是你对着错误提示查找解决方法,按照网页上的步骤,你对着提示符输入并执行了几条你完全不理解的命令,计算机又能正常启动了,但同时你发现你那存有大学舍友糗照的硬盘分区被清空了。

imred的专栏 3069

XXL-JOB的使用(详细教程)

欢迎访问我的个人博客:www.ifueen.com 文章目录概述使用 概述 ​ 首先我们要知道什么是XXL-JOB? ​ 官方简介:XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用 ​ XXL-JOB的有点特性: ​ 1、简单:支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手; ​ 2、动态:...

f2315895270的博客 22万+

3D高斯溅射融合Gabor滤波器:频率感知如何提升神经渲染效率与质量

在计算机视觉与图形学领域,3D场景重建旨在从多视角图像中恢复真实世界的三维结构与外观。其核心原理是通过优化场景表示,使渲染出的二维图像与输入照片尽可能一致。传统方法在效率与细节保真度间常面临权衡,高频细节(如精细纹理、锐利边缘)的重建尤为消耗算力。频率感知技术为此提供了突破方向,它借鉴信号处理思想,将场景分解为不同空间频率的成分进行针对性建模。Gabor滤波器作为经典的频率-方向分析工具,能有效刻画纹理的局部频域特征。将Gabor的频率选择性与3D高斯溅射(3DGS)这一高效显式表示结合,构成了神经Gabo

weixin_33087827的博客 272

泛微ecology9二次开发-附件上传接口实战:从表单创建到数据库存储解析

本文详细解析了泛微ecology9二次开发中的附件上传接口实战,从测试环境搭建、数据库表结构解析到接口代码实现,提供了全面的开发指南。重点介绍了文件上传的核心逻辑、常见问题排查及高级应用场景,帮助开发者高效实现附件上传功能并优化系统性能。

weixin_30836759的博客 477

xxljob 配置具体定时任务_快速使用分布式定时任务 xxl-job

快速使用分布式定时任务 xxl-job需要linux服务器环境安装: jdk1.8 ,docker安装步骤地址 :linux快速安装jdk 、在linux里安装docker1.docker安装mysql数据库网站 https://hub.docker.com/_/mysql?tab=tags查找mysql的版本,拉取镜像命令: docker pull mysql:5.7通过镜像运行容器命令...

weixin_39631017的博客 503

Adams与Matlab联合仿真入门:从零搭建你的第一个动力学模型

本文为Adams与Matlab联合仿真新手提供了一份详尽的入门指南。文章从软件环境配置、版本兼容性检查讲起,逐步演示了如何在Adams中构建单摆动力学模型并定义输入输出接口,以及在Matlab/Simulink中搭建控制回路。通过一个完整的受控单摆实例,手把手教你完成从模型导出、模块生成到执行联合仿真的全流程,帮助你快速搭建并运行第一个联合仿真模型,实现动力学与控制系统的协同验证。

g5h6i7j的博客 699

xxl-job源码解析(看这一篇就够了,超简约且详细)

服务端启动流程 首先找到配置类 XxlJobAdminConfig 可以发现该类实现 InitializingBean接口,这里直接看 afterPropertiesSet方法即可。 @Override public void afterPropertiesSet() throws Exception { //利用静态声明的只会加载一次的特性,初始化一个单例对象。 adminConfig = this; //初始化xxjob调度器

Nuan_Feng的博客 3万+

基于STM32F407标准库与SimpleFOC实现无刷电机2804位置闭环控制的实战解析

本文详细解析了基于STM32F407标准库与SimpleFOC算法实现2804无刷电机位置闭环控制的实战过程。通过硬件选型、速度闭环调试到位置环PID参数整定,提供了完整的工程实现方案和抗饱和策略,帮助开发者掌握高精度电机控制技术。

oauth7security的博客 918

YOLO26 网络结构逐模块解析:Backbone/Neck/Head

本文解析了YOLO26目标检测网络的架构设计,重点介绍其在Backbone、Neck和Head三个核心组件的创新优化。Backbone采用改进的CSP模块与轻量级注意力机制,提升特征提取效率;Neck使用改进的BiFPN结构实现高效多尺度特征融合;Head采用完全解耦设计简化回归流程。文章详细分析了各模块的技术演进、实现细节和设计思想,通过流程图和代码示例展示了YOLO26的整体架构和关键组件实现。该研究对模型优化、定制开发和硬件部署具有重要指导意义。

一键难忘的博客 1247

XXL-JOB源码分析

/doc :文档资料- /db :“调度数据库”建表脚本- /xxl-job-admin :调度中心(任务调度管理平台),项目源码- /xxl-job-core :公共Jar依赖(调度核心- /xxl-job-executor-samples :执行器, Sample示例项目 (大家可以在该项目上进行开发,也可以将现有项目改造生成执行器项目)

lzd649648959的专栏 2424

解锁PyCharm+Deepseek:本地部署AI编程超能力

在本次探索中,我们成功地在 PyCharm 中实现了 DeepSeek 的本地化部署,这一过程虽然涉及多个步骤,但每一步都为我们开启 AI 编程大门奠定了基础。从安装最新版 PyCharm,确保我们拥有一个功能强大且兼容性良好的开发环境,到注册 DeepSeek 账号并获取 API Key,这是连接我们与强大 AI 能力的关键纽带。接着,在 PyCharm 中安装 Continue 插件并进行精细配置,让 DeepSeek 能够深度融入我们的编程流程。

远程部署调试 运行安装 项目调试 二次开发 项目技术新 持续迭代 部分源码免费分享 5550

XXL-Job源码分析

XXL-Job源码分析

Zhuxiaoyu_91的博客 1349

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

分布式任务调度平台xxl job应用及源码分析--含本地集群

框架分类 现如今常见定时任务框架可按如下区分: 单机:Timer、ScheduledExecutorService、Spring定时框架 分布式:Quartz、SpringBatch、TBSchedule、elastic-job、xxl-job、Saturn 另外有些小众的框架:uncode-schedule、LTS、TBSchedule、Opencron、Antares、sia-task 常用框架特点: Quartz Java定时任务标准。但Quartz关注点在于定时任务而非数据,并无一套根据数据处理而定

Yuhei001的博客 2139

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

Xxl Job 源码分析(一) -- 启动调度中心

从之前的 [Xxl Job Helloworld](https://blog.youkuaiyun.com/u012410733/article/details/103933544) 中学会了简单的使用 ``Xxl-Job`` 进行分步式任务调度。并且可以知道当我使用 ``Xxl-Job``时,我们核心基本需要以下三个步骤: - 启动调度中心(xxl-job-admin) - 启动执行器(引用xxl-job-core 的业务代码) - 调度中心添加任务并执行任务

carl.zhao的专栏 1792

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

XXL-JOB源码梳理——一文理清XXL-JOB实现方案

在分布式环境下,一般会将定时任务拆解为任务调度部分和任务执行部分,各司其职。调度中心就是总览全局的Leader,具体的执行器就是需要工作的worker。

这有一片海的博客 2417

分布式定时任务系列7:XXL-job源码分析之任务触发

分布式定时任务系列7:XXL-job源码分-任务触发。

weigeshikebi的博客 1803

xxl-job框架的调度流程源码分析

xxl-job-admin调度中心流程 最近一直在研究xxl-job,所以就分析了下调度的流程,做个记录。 1.调度中心说明 调度中心主要负责提供UI界面进行任务的配置工作,配置好任务之后,只要点击了启动,任务就会发送给执行器进行执行。 2.调度中心任务调度源码分析 xxl-job-admin模块为springboot项目,springboot项目启动的时候会加载响应的配置,其中与任务调度相关的配置如下 代码来源:com.xxl.job.admin.core.conf.XxlJobAdminConfig 该

hanrentong的博客 901

XXL-JOB分布式任务调度框架(五)-源码分析-任务调度执行流程及实现原理

调度中心集群是否会产生重复调度问题?间隔多长时间查询一次数据库合适?如何判断任务已经到了触发时间?如果查询出的任务,已经过了触发时间了怎么办?如何获取执行器?有多个执行器怎么处理呢?任务执行时间过长,阻塞其他任务执行怎么处理?……

专业的计算机毕业设计指南 1454

XXL-JOB Admin端启动流程和调度算法源码

XXL-JOB 作为国内使用最广泛的轻量级分布式任务调度平台,其 Admin 端(调度中心) 的设计极具代表性。本文将深入到 XXL-JOB Admin 端的源码层面,完整梳理其启动流程与核心调度链路。从 XxlJobScheduler 的初始化,到 registryMonitorThread 的注册保活机制;从 慢任务探测与线程池隔离 的时间滑动窗口算法,到 十种路由策略(轮询、LRU、LFU、一致性 Hash 等)的底层实现,逐一拆解。

m0_71749752的博客 681

【xxl-job 学习之源码分析】

xxl-job 学习之原理分析 前言 大家在工作过程中,或许都对定时任务有所了解,那么业务需求多一点就会用特定的框架来实现(毕竟:专业的事交给专业的人去做)。因此就有了xxl-job 的出场机会,这里对于xxl-job 是什么,能做什么,如何使用不再赘述,此处只分析原理和实现机制。阅读对象:针对想了解xxl-job 源码和原理且拥有一定开发经验的朋友。文章源码所用版本: 2.3.1-SNAPSHOT 资料下载与说明 xxl-job 源码下载 下载完成后用 IDEA 打开,结构如上所示。详解每一部分内容如

Julie的博客 1204

分布式定时任务系列10:XXL-job源码分析之路由策略

** FIRST(第一个):固定选择第一个机器;*//** (最后一个):固定选择最后一个机器;*//** (轮询):;*//** (随机):随机选择在线的机器;*//** (一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。*//** (最不经常使用):使用频率最低的机器优先被选举;*//** (最近最久未使用):最久未使用的机器优先被选举;*//** (故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;

weigeshikebi的博客 2123

分布式定时任务系列5:XXL-job中blockingQueue的应用

分布式定时任务系列1:XXL-job安装分布式定时任务系列2:XXL-job使用分布式定时任务系列3:任务执行引擎设计分布式定时任务系列4:任务执行引擎设计续

weigeshikebi的博客 1780

xxl-job源码及技术方案分析

1. xxl-job源码及技术方案分析 分析的基础,是按照官网的文档,先有大概的认知:https://www.xuxueli.com/xxl-job/ 当前的版本是2.3.0 宏观方向把握xxl-job的架构 先摘抄一段内容 大致描述: xxl-job整体架构采用中心化设计,分为调度中心Admin和执行器两部分; 调度中心Admin模块提供trigger触发接口进行作业调度,然后根据作业历史统计下发耗时将作业分配到两个线程池中的一个进行执行; 执行前将作业启动日志记录到xxl_job

KelvinLiu 2080
上一篇: 分布式定时任务—xxl-job学习(二)——执行器的启动过程源码分析
下一篇: 分布式定时任务—xxl-job学习(四)——调度中心web页面端api调用源码分析
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RabbitsInTheGrass

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值