在上一篇《xxl-job调度平台之任务执行》中,我们知道了任务的一次执行流程。今天,我们一起来看看,任务是如何被触发的。
本文中源码,来自xxl-job 2.4.0版本
一 手动触发
在任务页面点击【执行一次】按钮,将立即触发执行。 对应后端接口如下:调用JobTriggerPoolHelper.trigger(),failRetryCount取-1。
二 定时触发
配置cron
表达式,将任务设置为启动后,将由调度平台周期性触发执行。 在调度平台启动时,启动了调度线程。
java
JobScheduleHelper.getInstance().start();
2.1 JobScheduleHelper类
该类启动了两个线程:scheduleThread、ringThread,前者循环周期长,后者循环周期短。
主要属性如下:
java
// 向后预读取时间,5秒
public static final long PRE_READ_MS = 5000; // pre read
// 处理已过期或5秒内将执行的任务,这些任务是从DB中查询的
private Thread scheduleThread;
// 整秒执行一次,处理ringData
private Thread ringThread;
private volatile boolean scheduleThreadToStop = false;
private volatile boolean ringThreadToStop = false;
// 时间轮,key是秒数(60以内的整数),value是jobId集合
private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();
2.2 scheduleThread线程
- 整秒处运行,两次间隔最多5秒,5秒内最多执行一次;
- 从库表
xxl_job_info
中查询trigger_next_time <= now + 5
且启动了的任务;这些任务可能已过点,也可能未过点。 - 如果任务已过点5秒以上时,根据"调度过期策略",选择忽略或立即执行一次;
- 如果任务过点在5秒内时,直接触发一下,如果下次执行时间在之后的5秒内,则将任务添加到
ringData
; - 如果任务将在5秒内到期,则将任务添加到
ringData
; - 根据
cron
计算下次执行时间,更新triggerLastTime、triggerNextTime属性。
来看看具体细节:
- 两次执行间最大间隔5秒
- 查询XxlJobInfo时,采用limit限制数量,默认为6000条。
- 当调度平台集群部署时,为了防止节点间并发,使用
select for update
加表行锁;加锁成功才继续执行; - 在查询时,预读取了5秒内到期的任务;
- 假设新建任务,从0点起每12小时执行一次,trigger_next_time默认为0;00:20时将它启动:
- 如果调度过期策略设置为"忽略",那得等到12点才首次执行;
- 如果调度过期策略设置为"立即执行一次",将在几秒内完成首次执行;
- 5秒内将到期的任务,不能立即执行;放入ringData中,由ringThread处理;
- 调度类型分为cron、固定速率两种,前者依赖于
CronExpression
计算下次执行时间。
2.3 ringThread线程
- 整秒处运行,两次间隔最多1秒,1秒内最多执行一次;
- 获取当前时间的秒数N,则
N >= 0 && N < 60
,从ringItemData
中查找N映射的jobId集合,然后一个个触发执行。
来看看具体实现:
- 校准为整秒时执行一次
- 从ringData中查找当前秒数,及过期1秒的任务
- 遍历触发
2.4 时间轮
其实,ringData的Map结构,就是时间轮(图片来自网络):
- tickMs=1秒,
- wheelSize=60,在0-59的格子中放入这一秒到期的所有任务。
- currentTime即当前时间的秒数,只是xxl-job向前增加了一个跨度;假设现在是第31秒,则获取31、30两个格子的任务。
java
// 当前秒数
int nowSecond = Calendar.getInstance().get(Calendar.SECOND)
- tickMs(基本时间跨度) :时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度(tickMs)。
- wheelSize(时间单位个数) :时间轮的时间格个数是固定的,可用(wheelSize)来表示;
- currentTime(当前所处时间) :时间轮还有一个表盘指针(currentTime),用来表示时间轮当前所处的时间,currentTime 是 tickMs 的整数倍。currentTime 当前指向的时间格,表示刚好到期,需要处理此时间格所对应的所有任务。
三 总结
- 两个线程,一个线程查表,获取将到期任务,放入时间轮中,交由另一个线程来处理;这种设计在定时任务的实现中常被采用;
- 采用DB表行锁,在调度平台多节点间,避免并发调度;
- 调度过期策略,是有业务价值的;
- 任务执行可能出现一个周期的延迟 :
- 如调度平台节点A,将5秒内到期的任务放入ringData,然后宕机了,且TriggerNextTime已被修改;
- 节点B紧接着执行时,得等到下一个到期时间,才能查询到这个任务;
- 举例:某任务(周期1小时)20:55:00到期,20:54:56时被节点A查到,放入ringData,更新triggerLastTime为21:55:00,但20:54:58时节点A宕机了。该任务只能等到21:55:00才被执行。