xxl-job源码分析(三):admin启动和任务触发

620 阅读4分钟

通过上一节 业务方接入启动流程可以看到,业务方会启动一些线程,以一定的频率调用 /api/callback,/api/registry,/api/registryRemove,本章就讲解admin启动的流程和怎么调用定时任务

一.admin启动的核心

admin就是一个普通的springboot项目,启动后有如下几个核心类

1.1.JobApiController

image.png

1.1.1 callback方法

方法很简单,就是从xxl_job_log表中找到这个日志,同步执行的结果

1.1.2 registry方法

将执行器的参数,添加到xxl_job_registry表中

1.1.3 registryRemove方法

将执行器,从表xxl_job_registry中删除

1.2 JobInfoController类

备注:该类主要是对任务的增删改查操作 核心方法

1.2.1 添加任务/jobinfo/add

这个方法就是保存页面的任务参数到xxl-job-info表

image.png

1.2.2 启动/暂停任务 /jobinfo/start|stop

概要: 这一块都比较简单,都是一些常见的crud

1.3 初始化启动线程,执行相应的任务

XxlJobAdminConfig类的
public void afterPropertiesSet() throws Exception {
    adminConfig = this;

    xxlJobScheduler = new XxlJobScheduler();
    xxlJobScheduler.init();
}
public void init() throws Exception {
    // 1.多语言初始化
    initI18n();

    //初始化fastTriggerPool和slowTriggerPool线程池对象
    JobTriggerPoolHelper.toStart();

    /**
     * 开启线程,每90s查询执行器的数据,如果执行器上次更新时间超过90s未更新,就移除这个执行器,并把存活的执行器更新
     */
    JobRegistryHelper.getInstance().start();

    /**
     * 启动线程,查找任务执行失败的任务,
     * 1.设置了重试次数,就再次触发任务
     * 2.判断是否需邮件预警
     */
    JobFailMonitorHelper.getInstance().start();

    /**
     * 启动线程,处理任务结果丢失的数据
     * 任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败;
     */
    JobCompleteHelper.getInstance().start();

    JobLogReportHelper.getInstance().start();

    /**
     * todo 执行任务(核心)
     * 启动线程,执行任务
     */
    JobScheduleHelper.getInstance().start();

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

这些核心步骤,会在第三章中讲解

二.任务定时触发

image.png

在 ** 1.3 初始化启动线程,执行相应的任务**中,看到会启动很多线程,执行不同的事情,最重要的是最后一步

JobScheduleHelper.getInstance().start();

接下来我们就来进行分析

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、TODO 查询从当前时间+5秒内要执行的任务
                    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 todo 如果任务超时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 todo 如果任务配置的"调度过期策略"是"立即执行一次",那么就触发一次任务
                                    JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null);
                                    logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );
                                }

                                // 2、fresh next todo 从当前时间开始,计算任务的下一次执行时间
                                refreshNextValidTime(jobInfo, new Date());

                            } else if (nowTime > jobInfo.getTriggerNextTime()) {
                                // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time todo 任务执行时间在当前时间的5s内

                                // 1、trigger TODO 触发任务
                                JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
                                logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );

                                // 2、fresh next todo 从当前时间开始,计算任务的下一次执行时间
                                refreshNextValidTime(jobInfo, new Date());

                                // next-trigger-time in 5s, pre-read again todo 如果下一次的执行时间在未来5s内
                                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 {
                                //TODO 还没有到达任务执行的时间
                                // 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time

                                // 1、TODO 计算剩余的秒数字
                                int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60);

                                // 2、TODO 把剩余秒数--任务id存入map中 ;==>下面的 ringThread 线程,会每一秒执行一次,查到对应的数据后,触发任务
                                pushTimeRing(ringSecond, jobInfo.getId());

                                // 3、TODO重新计算下一次时间
                                refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));

                            }

                        }

                        // 3、update trigger info TODO 修改jonInfo的内容
                        for (XxlJobInfo jobInfo: scheduleList) {
                            XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo);
                        }

                    } else {
                        preReadSuc = false;
                    }

这一段代码比较长,注释已经添加到代码中了,核心就是做了几件事

  • 查询任务触发时间 < 当前时间+5秒的任务
  • 如果任务超时5秒没有执行,但是如果任务配置的"调度过期策略"是"立即执行一次",那么就触发一次任务
  • 如果任务+5秒<当前时间,就进行触发,并计算下一次执行的时间
  • 如果任务还没到时间,就不触发

接着,我们进入触发任务代码

JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);

接着进入

public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam, String addressList) {
    helper.addTrigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
}

接着会放入线程池中执行

com.xxl.job.admin.core.thread.JobTriggerPoolHelper#addTrigger


XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);

最终会调用 com.xxl.job.admin.core.trigger.XxlJobTrigger#processTrigger

这个方法核心的步骤

image.png

executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList());

image.png

常用的路由策略逻辑如下

  • 第一个

image.png

  • 最后一个

同理选addressList的最后一个元素

  • 轮询

image.png

-故障转移

image.png

从上面常用的路由策略,可以看到逻辑都比较简单

路由策略选好地址后,接下来就是调用

 runExecutor(triggerParam, address);
@Override
public ReturnT<String> run(TriggerParam triggerParam) {
//发送http请求,调用执行器的run方法
    return XxlJobRemotingUtil.postBody(addressUrl + "run", accessToken, timeout, triggerParam, String.class);
}