文章目录
前言
本文对JobScheduleHelper 的工作内容进行介绍。
一、JobScheduleHelper 作用:
JobScheduleHelper是xxl-job-admin中的辅助类,其主要作用是进行任务的调度和执行管理。具体作用包括:
-
任务调度:JobScheduleHelper负责根据任务的调度策略和触发规则,对任务进行调度,确定任务的执行时间和执行方式。
-
任务执行管理:JobScheduleHelper负责管理任务的执行过程,包括任务的准备、执行、结束等阶段,确保任务能够按时正确执行。
-
监控任务执行情况:JobScheduleHelper能够监控任务的执行情况,包括任务执行结果、执行时间、执行状态等,以便及时发现和处理问题。
-
处理任务异常:当任务执行出现异常情况时,JobScheduleHelper会根据预设的处理策略来进行处理,保证任务执行的可靠性。
-
调度任务执行者: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 的源码内容进行介绍。