原理篇-- 定时任务xxl-job-服务端(admin)项目启动过程--JobScheduleHelper初始化 (8)


前言

本文对JobScheduleHelper 的工作内容进行介绍。


一、JobScheduleHelper 作用:

JobScheduleHelper是xxl-job-admin中的辅助类,其主要作用是进行任务的调度和执行管理。具体作用包括:

  1. 任务调度:JobScheduleHelper负责根据任务的调度策略和触发规则,对任务进行调度,确定任务的执行时间和执行方式。

  2. 任务执行管理:JobScheduleHelper负责管理任务的执行过程,包括任务的准备、执行、结束等阶段,确保任务能够按时正确执行。

  3. 监控任务执行情况:JobScheduleHelper能够监控任务的执行情况,包括任务执行结果、执行时间、执行状态等,以便及时发现和处理问题。

  4. 处理任务异常:当任务执行出现异常情况时,JobScheduleHelper会根据预设的处理策略来进行处理,保证任务执行的可靠性。

  5. 调度任务执行者:JobScheduleHelper负责将任务分配给合适的任务执行器(JobExecutor)执行,实现任务的负载均衡和高效执行。

总的来说,JobScheduleHelper在xxl-job-admin中扮演着任务调度和执行管理的重要角色,确保任务按时正确执行,提高系统的稳定性和效率。通过JobScheduleHelper的配合,xxl-job-admin能够更好地管理和调度任务,实现任务的自动化执行和监控。

二、使用步骤

2.1 start() 初始化:

2.1.1 任务的调度:

// schedule thread
// 任务调度 scheduleThread  线程创建
scheduleThread = new Thread(new Runnable() {
   @Override
   public void run() {

       try {
       		// 睡眠 4.x 秒被唤醒 
           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();
			// 数据库连接,执行sql 的 PreparedStatement 对象声明
           Connection conn = null;
           Boolean connAutoCommit = null;
           PreparedStatement preparedStatement = null;

           boolean preReadSuc = true;
           try {
				// 获取数据库连接,设置自动提交为false
               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();
               // 读取 preReadCount 条 未来( PRE_READ_MS = 5000)5s 内要执行的任务信息
               /**
               * 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}
               **/
               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 如果当前时间已经在 该任务下次执行器 +5s 之前的任务
                       // 表示任务到了时间没有被触发,则失火
                       if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
                           // 2.1、trigger-expire > 5s:pass && make next-trigger-time
                           logger.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = " + jobInfo.getId());

                           // 1、misfire match 获取任务设置的失火策略
                           MisfireStrategyEnum misfireStrategyEnum = MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), MisfireStrategyEnum.DO_NOTHING);
                           if (MisfireStrategyEnum.FIRE_ONCE_NOW == misfireStrategyEnum) {
                           		// 如果失火策略是立即补偿一次则 触发任务的执行
                               // FIRE_ONCE_NOW 》 trigger
                               JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.MISFIRE, -1, null, null, null);
                               logger.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = " + jobInfo.getId() );
                           }

                           // 2、fresh next 刷新任务的下次执行时间
                           refreshNextValidTime(jobInfo, new Date());

                       } else if (nowTime > jobInfo.getTriggerNextTime()) {
                       		// 如果任务下次执行的时间 是在当前时间-5秒内的,意味着这个任务是正常的,则正好去执行任务
                           // 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time

                           // 1、trigger
                           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  如果这个任务在接下来的5s 内还有执行
                           // 则将任务放入到 刻度 为1-59 的时间轮中,由时间轮线程触发任务的执行
                           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
							// 当前任务是在5s 内要执行,但是现在还没有到 执行的时间则放入时间轮中进行任务的触发
                           // 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 
                   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
           // 如果耗时小于1s 则有可能是本次扫描到的任务很少,甚至没有扫描到任务
           // 如果大于1000 则证明现在需要较多的任务需要去执行,则线程不需要进行休眠,继续下一次循环
           // 如果小于1000 则根据本次扫描到的任务数量判断 睡眠多久后进行下次扫描
           //   如果本次扫描没有扫描到任务则 休眠 PRE_READ_MS = 5000 5s 进行下次扫描
           //   如果每次扫描到有任务则休眠 0.x 秒 进行下次扫描
           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();

2.1.2 ringData 时间轮任务的执行:

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

         while (!ringThreadToStop) {

             // align second  每次循环0.x s 达到在下一秒到来时 被唤醒
             try {
                 TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
             } catch (InterruptedException e) {
                 if (!ringThreadToStop) {
                     logger.error(e.getMessage(), e);
                 }
             }

             try {
                 // second data
                 List<Integer> ringItemData = new ArrayList<>();
                 // 避免处理耗时太长,跨过刻度,向前校验一个刻度;
                 int nowSecond = Calendar.getInstance().get(Calendar.SECOND);   
                 for (int i = 0; i < 2; i++) {
                 	// 从时间轮获取任务 这里循环了2次 避免跨过时间刻度,
                 	// 因为每次执行完改时间刻度的任务都会进行改刻度先任务的移除 ,所有不用担心任务的重复
                     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);
                 }
             }
         }
         logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
     }
 });
 // 设置时间轮 线程为守护线程,设置线程的名称,启动线程
 ringThread.setDaemon(true);
 ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
 ringThread.start();

2.1.3 ringData和scheduleThread 是否会重复执行:

答案是不会有认为被重复执行;从以上代码我们可以看到任务的执行主要借助scheduleThread 调度线程和 ringData 时间轮线程进行任务的触发;要想了解同一条任务 是否被重复执行,则主要看改任务 在下次触发时间到来的时候,会不会同时被scheduleThread 和 ringData 线程都触发;

既然scheduleThread 线程是查询未来5s 内要执行的任务,那么来分析下 一条任务 job 每隔 4s 执行一次的情况:

  • 首先在 scheduleThread 线程中 第一次扫描到未来 5s 要执行的任务,此时改job 被扫描到;
  • 如果改job 任务没有失火,如果当前还没有达到改任务的触发时间则直接将改任务放入到ringData 时间轮线程中进行执行,并更下次任务的执行时间;
  • 如果改job 任务没有失火,但是改任务下次执行器的时间 ,在当前时间-5s 内, 则正常触发一次任务的执行,然后刷新任务下次的执行时间,然后发现改任务下次的执行时间是在5s 内,又会将改任务放入到ingData 时间轮线程中进行执行;
  • scheduleThread 和ringData 时间轮线程相互协作完成任务的调度执行;

2.1.4 任务下次执行时间获取:

private void refreshNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception {
// 获取任务的下次执行的合法时间
   Date nextValidTime = generateNextValidTime(jobInfo, fromTime);
   if (nextValidTime != null) {
   	 // 设置任务的下次执行时间
       jobInfo.setTriggerLastTime(jobInfo.getTriggerNextTime());
       jobInfo.setTriggerNextTime(nextValidTime.getTime());
   } else {
   		// 没有获取到下次的执行时间
       jobInfo.setTriggerStatus(0);
       jobInfo.setTriggerLastTime(0);
       jobInfo.setTriggerNextTime(0);
       logger.warn(">>>>>>>>>>> xxl-job, refreshNextValidTime fail for job: jobId={}, scheduleType={}, scheduleConf={}",
       jobInfo.getId(), jobInfo.getScheduleType(), jobInfo.getScheduleConf());
   }
}
public static Date generateNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception {
	// 获取调度类型是按照cron 表达式,还是固定速度,计算下次任务的执行时间
    ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null);
    if (ScheduleTypeEnum.CRON == scheduleTypeEnum) {
        Date nextValidTime = new CronExpression(jobInfo.getScheduleConf()).getNextValidTimeAfter(fromTime);
        return nextValidTime;
    } else if (ScheduleTypeEnum.FIX_RATE == scheduleTypeEnum /*|| ScheduleTypeEnum.FIX_DELAY == scheduleTypeEnum*/) {
        return new Date(fromTime.getTime() + Integer.valueOf(jobInfo.getScheduleConf())*1000 );
    }
    return null;
}

2.1.4 ringData 时间轮任务:

private void pushTimeRing(int ringSecond, int jobId){
   //  private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();
   // 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) );
}

2.2 toStop() 释放资源:

public void toStop(){

    // 1、stop schedule scheduleThreadToStop 标识置为ture 使其跳出while 循环
     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 使其跳出时间轮线程的while 循环
     ringThreadToStop = true;
     try {
         TimeUnit.SECONDS.sleep(1);
     } catch (InterruptedException e) {
         logger.error(e.getMessage(), e);
     }
     if (ringThread.getState() != Thread.State.TERMINATED){
        // 如果线程不是终止状态,则等待线程任务的执行完成
         // interrupt and wait
         ringThread.interrupt();
         try {
             ringThread.join();
         } catch (InterruptedException e) {
             logger.error(e.getMessage(), e);
         }
     }

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

总结

本文对JobScheduleHelper 的源码内容进行介绍。

  • 20
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值