xxl-job源码分析之----admin server端分析

一、介绍

上一篇xxl-job源码分析之----XxlJobSpringExecutor分析已经介绍了client 端jar xxl-jar-core 的整体逻辑,这里主要介绍admin 端主要逻辑.

二、源码分析

从XxlJobAdminConfig 这里开始分析, XxlJobAdminConfig 实现了InitializingBean, DisposableBean ,重写了 afterPropertiesSet 和 destroy 方法.

2.1 XxlJobAdminConfig

public class XxlJobAdminConfig implements InitializingBean, DisposableBean {

    private XxlJobScheduler xxlJobScheduler;

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

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

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

2.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.");
    }

2.2.1 XxlJobScheduler#initI18n

这里主要是进行国际化,对ExecutorBlockStrategyEnum 枚举里面的元素 title 进行替换

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

2.2.2 JobRegistryMonitorHelper

JobRegistryMonitorHelper 这个线程主要是更新、清除注册实例,间隔30s ,所以xxl-job 有一定的延时, 这里主要逻辑如下:

  1. 获取所有的自动注册 的 执行器
  2. 获取所有的更新时间是90S 之前的job ,这些Job 可以认定为已经是死JOB,从DB里面删除.
  3. 获取更新时间是在90s 之内的job,刷新在线地址
  4. 并更新执行器里面的服务器列表地址
public void start(){
		registryThread = new Thread(new Runnable() {
			@Override
			public void run() {
				while (!toStop) {
					try {
						/** 获取自动注册的xxlJobGroup
						addressType:执行器地址类型:0=自动注册、1=手动录入
						**/
						List<XxlJobGroup> groupList = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0);
						if (groupList!=null && !groupList.isEmpty()) {

							// 除去90s 内没有更新的job
							/**
							**/
							List<Integer> ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date());
							if (ids!=null && ids.size()>0) {
								XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids);
							}

							// 刷新在线活跃地址 (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);
									}
								}
							}

							// 更新执行器对应的服务器列表
							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();
	}

2.2.3 JobFailMonitorHelper

JobFailMonitorHelper 这里主要是10s 一次,对失败的job 进行补偿,主要的逻辑如下:

  1. 从 日志记录表xxl_job_log 里面 捞出 运行失败的logId
  2. 对这些失败的logid 进行遍历, 由于有多台机器,所以先锁住log,获取到锁
  3. 锁住之后,根据logid 获取log 记录的所有信息
  4. 根据记录的log信息,获取jobid ,然后拿到信息, 看一下 对应的job 是否有设置重试次数,如果有便再次触发
  5. 查看job 配置的告警信息,如果有有配置,则对应相关的处理
  6. 更新,释放锁
public void start(){
		monitorThread = new Thread(new Runnable() {

			@Override
			public void run() {

				// monitor
				while (!toStop) {
					try {
                        // 获取前1000 条失败的log 日志记录
						List<Long> failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findFailJobLogIds(1000);
						if (failLogIds!=null && !failLogIds.isEmpty()) {
							for (long failLogId: failLogIds) {

								//  每一条日志,先尝试获取到锁
								int lockRet = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, 0, -1);
								if (lockRet < 1) {
									continue;
								}
								XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(failLogId);
								XxlJobInfo info = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(log.getJobId());

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

2.2.3 JobLosedMonitorHelper

JobLosedMonitorHelper主要就是 任务结果丢失处理:调度记录停留在 “运行中” 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败.

2.2.4 JobTriggerPoolHelper

JobTriggerPoolHelper 主要就是初始化两个线程池fastTriggerPool 和 slowTriggerPool 以及 add trigger,这里就主要分享add trigger

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

        /** 选择线程池,默认是快速触发线程池
        如果  一分钟之内 调用的次数超过10次,那就放到 慢触发线程池里
        **/
        ThreadPoolExecutor triggerPool_ = fastTriggerPool;
        AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
        if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) {   
            triggerPool_ = slowTriggerPool;
        }

        //触发调用
        triggerPool_.execute(new Runnable() {
            @Override
            public void run() {

                long start = System.currentTimeMillis();

                try {
                    // 进行调用触发
                    XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                } finally {

                    // 这里主要是为了确认当时时间是否还是在一分钟之内,如果已经超过一分钟,那就把当前的数据清空,重新统计
                    long minTim_now = System.currentTimeMillis()/60000;
                    if (minTim != minTim_now) {
                        minTim = minTim_now;
                        jobTimeoutCountMap.clear();
                    }

                    //统计 调用次数
                    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();
                        }
                    }

                }

            }
        });
    }

2.2.5 JobLogReportHelper

JobLogReportHelper 主要是为了呈现报表相关的逻辑,JobLogReportHelper 主要做了如下几件事:

  1. 统计最近三天的job 运行的情况,指标分别是: 运行中的, 成功次数, 失败次数 ,并将结果统计放入 xxl_job_log_report 中, 便于页面显示
  2. 清理log , 根据设置的日志保存天数,将超过保存天数的日志清除掉, 这里是一天运行一次
  3. 整个线程运行时间是间隔 1分钟

在这里插入图片描述

这里有几点: 1. 每间隔1分钟,就 重新统计一次最近3天的成功,失败,进行中的 统计, 如果调度中心的server 比较多, 那就是每一台机器都会运行这块逻辑,可以在此加一个分布式锁
4. 每隔一分钟统计一次,最近三天的相关数据, 统计当天的是有这个必要的, 但是频繁统计 前两天的感觉有点浪费资源了,毕竟前两天的基本上不会改变,只是有一些失败的重新运行的需要调整, 如果 job 运行正常,这块就是 一直重复运行,是否可以把这块的频率降低
5. 清除日志,可以也加一个 分布式锁, 没有必要所有的server 都运行相同的逻辑, order by id asc LIMIT 在日志量比较大时,性能还是不太好的

代码就不贴了,普通的业务处理

2.2.6 JobScheduleHelper

JobScheduleHelper 类应该是核心类了, 主要逻辑如下:

  1. 确定 每次预读取的数据量,一般是(triggerPoolFastMax+triggerPoolSlowMax) * 20
  2. 尝试获取锁,“select * from xxl_job_lock where lock_name = ‘schedule_lock’ for update”
  3. 从表中获取 now+ 5s 之内的一定数据
  4. 对每一个job 进行判断,根据下一次的触发时间
    1. 如果和当前时间对比,过期时间超过5s ,直接放弃,并刷新下一次运行时间
    2. 和当前时间相比,过期了,但是没有超过5s, 直接运行,并刷新下一次运行时间,如果下一次运行时间还在 当前时间 +5s 之内,加入 ring Time 里面的Map
  5. 剩下的情况就是没有过期, 需要在5s 只能正常运行的,那就加入到 ring Time 里面的Map
  6. 更新对应的 jobInfo
  7. 释放锁
  8. 如果运行失败,暂停5s, 如果运行成功,基本保持1s 间隔.

从上面看,如果server 比较多时,这样就会一直在不停的扫描表,数据库有一定的压力

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

                // 预读取数量, 这里是通过 (triggerPoolFastMax+triggerPoolSlowMax) * 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

                        // 预读取,获取 5s 之前的需要运行的job
                        long nowTime = System.currentTimeMillis();
                        List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
                        if (scheduleList!=null && scheduleList.size()>0) {

                            for (XxlJobInfo jobInfo: scheduleList) {

                                // 第一种情况, 运行job 过期超过5s , 那就直接跳过,并重新设置下一次运行的时间
                                if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
                                    logger.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = " + jobInfo.getId());
                                    refreshNextValidTime(jobInfo, new Date());

                                } else if (nowTime > jobInfo.getTriggerNextTime()) {
                                   // 第二种情况 过期时间小于5s, 直接触发,并 设置下一次触发时间
                                    // 1、直接调用
                                    JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
                                    logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );

                                    // 2、更新下一次运行时间
                                    refreshNextValidTime(jobInfo, new Date());

                                    // 再次校验, 如果下一次运行时间在 5s 之内,再次进行预读取
                                    if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {

                                        // 这里设置了一个0-60的 环, 确认放入的 对应的位置
                                        int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                        // 2、放入 对应的Map 中
                                        pushTimeRing(ringSecond, jobInfo.getId());

                                        // 3、再次刷新下一次运行时间
                                        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                                    }

                                } else {
                                    // 第三种情况就是 按照正常预期运行的情况
                                    // 1、确定对应的时间段
                                    int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);
                                    // 2、放入Map
                                    pushTimeRing(ringSecond, jobInfo.getId());
                                    // 3、刷新下一次
                                    refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
                                }

                            }

                            // 3、更新
                            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;


                    /**
                    如果运行时间小于1s,稍微暂停一下,基本上是1s一次的job
                    如果是失败的,那就等5s
                    **/
                    if (cost < 1000) {  
                        try {
                            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");
            }

另外一个线程ringThread ,其主要逻辑就是:
每隔1s 从Map 里面获取跨度为 2s 的数据,进行调度运行.
避免处理耗时太长,跨过刻度,向前校验一个刻度;主要是为了避免当时刚刚加进来, 导致要再等60s
例如: 当前时间是 1s, 刚从Map 里面获取了对应的jobId, 这时又一个加进来, 这样就要再等60s
才能执行, 现在跨度为2个s, 到了下一秒的时候,再次获取上一秒的数据,做了一个保护作用

        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);   
                        /** 避免处理耗时太长,跨过刻度,向前校验一个刻度;主要是为了避免当时刚刚加进来, 导致要再等60s
                        例如: 当前时间是 1s, 刚从Map 里面获取了对应的jobId, 这时又一个加进来, 这样就要再等60s 
                        才能执行, 现在跨度为2个s, 到了下一秒的时候,再次获取上一秒的数据,做了一个保护作用
                        **/
                        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");
            }
        });

2.3 XxlJobTrigger

XxlJobTrigger 类是进行触发调用的核心,就是结合 job info 配置的相关策略 进行 Http 请求.

三、小结

基本上 admin 端的整体流程分析完了, 还有其他的 如cronExpression 这块就暂不分析了.

支付宝微信
支付宝微信
如果有帮助记得打赏哦特别需要您的打赏哦
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一直打铁

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

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

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

打赏作者

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

抵扣说明:

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

余额充值