什么是时间轮
时间轮出自Netty
中的HashedWheelTimer
,是一个环形结构,可以用时钟来类比,钟面上有很多bucket
,每一个bucket
上可以存放多个任务,使用一个List
保存该时刻到期的所有任务,同时一个指针随着时间流逝一格一格转动,并执行对应bucket
上所有到期的任务。任务通过取模决定应该放入哪个bucket
。和HashMap
的原理类似,newTask
对应put
,使用List
来解决 Hash
冲突。
以上图为例,假设一个bucket
是1秒,则指针转动一轮表示的时间段为8s,假设当前指针指向 0,此时需要调度一个3s后执行的任务,显然应该加入到(0+3=3)的方格中,指针再走3s次就可以执行了;如果任务要在10s后执行,应该等指针走完一轮零2格再执行,因此应放入2,同时将round(1)
保存到任务中。检查到期任务时只执行round
为0的,bucket
上其他任务的roun
d减1。
存入时间轮
// 时间轮的数据结构
private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();
// 存入时间轮
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));
}
上面是处理存储的方法,接下来通过注释来表示,执行器怎么执行的
//获取当前时间(ms)
long nowTime = System.currentTimeMillis();
// 获取未来5s内需要执行的任务
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(意思是任务已经超期,因为只执行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());
// fresh next
//刷新下次执行时间
refreshNextValidTime(jobInfo, new Date());
} else if (nowTime > jobInfo.getTriggerNextTime()) {
//如果任务在5s内
// 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>下次执行时间(该次为未来时间需要执行)
if (jobInfo.getTriggerStatus() == 1
&& nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {
// 1、make ring second
//获取未来需要执行的秒数(如果执行时间为 2021/1/29 17:03:26 则返回26)
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;
}
接下来用时间轴来解释下执行器的执行条件
- 条件1
nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS
- 条件2
nowTime > jobInfo.getTriggerNextTime()
- 条件3
jobInfo.getTriggerStatus() == 1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()
- other
通过如上条件完成对小于5s内的所有任务的处理·
总结
通过阅读源码发现在临界情况(极端量级)下存在任务漏调度或者不调度的情况,不知道其他调度器是否存在,如有错误敬请斧正