分布式定时任务调度系统
流程分析
一个分布式定时任务,需要具备有以下几点功能:
- 核心功能:定时调度、任务管理、可观测日志
- 高可用:集群、分片、失败处理
- 高性能:分布式锁
- 扩展功能:可视化运维、多语言、任务编排
在分布式环境下,一般会将定时任务拆解为任务调度部分和任务执行部分,各司其职。
调度中心就是总览全局的Leader
,具体的执行器就是需要工作的worker
。Leader
分配任务,worker
负责任务执行。那么Leader
会等worker
执行完任务再去干其他事情吗?显然不行,这样效率太低了。
Leader :时间到了,你去执行任务
Worker :收到,我马上执行,任务完成给你反馈,不用一直等我。。。
Leader :任务执行完了
Worker :收到
Worker:执行器挂了??任务也标记失败吧。。。还得报告上级,任务失败了。。
核心问题
- 任务如何触发?触发失败的处理逻辑?
- 任务如何执行?任务结果如何反馈?反馈回调失败处理逻辑?任务日志查看?
- 任务失败判断逻辑与依据,任务失败后告警提示?
- 如何保证高可用?集群如何搭建?
- 调度和执行之间的通信和心跳?
同类产品对比
QuartZ | xxl-job | SchedulerX 2.0 | PowerJob | |
---|---|---|---|---|
定时类型 | CRON | CRON | CRON、固定频率、固定延迟、OpenAPI | CRON、固定频率、固定延迟、OpenAPI |
任务类型 | 内置Java | 内置Java、GLUE Java、Shell、Python等脚本 | 内置Java、外置Java(FatJar)、Shell、Python等脚本 | 内置Java、外置Java(容器)、Shell、Python等脚本 |
分布式任务 | 无 | 静态分片 | MapReduce 动态分片 | MapReduce 动态分片 |
在线任务治理 | 不支持 | 支持 | 支持 | 支持 |
日志白屏化 | 不支持 | 支持 | 不支持 | 支持 |
调度方式及性能 | 基于数据库锁,有性能瓶颈 | 基于数据库锁,有性能瓶颈 | 不详 | 无锁化设计,性能强劲无上限 |
报警监控 | 无 | 邮件 | 短信 | 邮件,提供接口允许开发者扩展 |
系统依赖 | 关系型数据库(MySQL、Oracle...) | MySQL | 人民币 | 任意 Spring Data Jpa支持的关系型数据库(MySQL、Oracle...) |
DAG 工作流 | 不支持 | 不支持 | 支持 | 支持 |
数据来源于
PowerJob
:https://www.yuque.com/powerjob/guidence/intro
XXL-JOB相关概念
调度中心 :
xxl-job-admin
;统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。
执行器:负责接收调度中心的调度并执行;可直接部署执行器,也可以将执行器集成到现有业务项目中。
XXL-JOB系统架构
逻辑架构
数据架构
xxl-job
调度中心数据表
- xxl_job_lock:任务调度锁表;
- xxl_job_group:执行器信息表,维护任务执行器信息;
- xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等;
- xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;
- xxl_job_log_report:调度日志报表:用户存储XXL-JOB任务调度日志的报表,调度中心报表功能页面会用到;
- xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;
- xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;
- xxl_job_user:系统用户表;
核心表E-R图
执行流程
整体执行流程
- 执行器自动注册到调度中心,
30
秒执行一次,用于心跳检查。执行器销毁会取消注册。 - 调度中心根据触发时间触发调度任务。
- 执行器通过任务执行线程池执行任务,并记录执行日志,执行结果异步上报。
- 调度中心日志请求。
执行流程细化
执行器注册:
- 服务启动后
ExecutorRegistryThread
通过registryThread
注册线程每30s
向调度中心注册一次。服务销毁后,ExecutorRegistryThread
调用一次取消注册接口,从调度中心删除当前节点的注册信息。JobRegistryHelper
内部维护了一个registryOrRemoveThreadPool
注册或者删除线程池,用于处理执行器客户端发送的注册或者删除请求,同时更新调度中心的执行器地址信息。JobRegistryHelper
内部为了一个registryMonitorThread
注册监听线程,每30s
执行一次(与客户端注册频率一致),用于监听超过90s
未主动注册的节点地址。超过90s
就认为节点掉线。调度中心任务调度:
JobScheduleHelper
主要负责任务调度逻辑判断与执行调度。内部维护了来个线程来执行任务调度。scheduleThread
调度线程:主要负责扫描任务,将能够执行的任务放入时间轮,并计算下一次执行时间。ringThread
时间轮线程:主要处理时间轮 中的任务,调用JobTriggerPoolHelper
进行任务触发。JobTriggerPoolHelper
任务触发线程,由快慢线程池组成,根据任务触发时间来进行切换选择由哪一个线程池触发任务。任务触发器根据任务信息组装触发参数(包括基本信息和阻塞策略),任务触发器根据任务配置的路由策略进行路由寻址,然后通过远程调用进行任务触发。XxlJobTrigger
主要负责任务触发执行动作。ExecutorBizClient
是ExecutorBiz
接口的客户端sdk
实现,在调度中心使用,相当于执行器的sdk
,调用执行器的Rest
接口使用。同理ExecutorBizImpl
就是ExecutorBiz
执行器业务逻辑实现。- 调度中心的
http
服务就是Spring Boot
实现的JobApiController
层执行器执行任务:
- 执行器中的
http
服务是通过netty
搭建的。ExecutorBizImpl
接收到触发任务后先根据阻塞策略和任务类型进行必要参数组装,组装完成后交给XxlJobExecutor
处理,XxlJobExecutor
通过registJobThread()
方法获取执行线程同时启动线程,然后将触发任务信息放入任务队列,由线程消费处理。JobThread
任务线程,负责执行任务,记录执行日志到**文件,**任务执行完毕后,将结果推送到TriggerCallbackThread
的callBackQueue
回调队列中,由TriggerCallbackThread
负责任务结果回调。TriggerCallbackThread
主要负责任务执行结果回调,将执行结果反馈给调度中心。TriggerCallbackThread
内部维护了triggerCallbackThread
和triggerRetryCallbackThread
两个线程。triggerCallbackThread
负责处理callBackQueue
队列中的数据,回调失败将回调参数记录到回调日志文件中,一直执行。triggerRetryCallbackThread
主要对回调失败的数据进行重试,每30s
执行一次,主要动作:将回调日志读取出来,反序列化后执行调用。调度中心任务结果处理:
AdminBizImpl
基本没做复杂逻辑,接收到客户端发送的回调结果后,直接交给JobCompleteHelper
处理。JobCompleteHelper
负责对任务执行结果处理,内部维护了一个线程池和一个线程。callbackThreadPool
线程池主要负责异步处理执行结果。monitorThread
主要处理未收到回调的任务,60s
执行一次,判断条件:①任务状态处于运行中超过10min
并且 ②执行器不在线。也就是说在线的执行器,任务执行超过10min
不会标记为失败。
服务端启动流程
服务端执行时序图
🔔主要流程:
- 任务执行调度器负责计算任务是否需要执行,将需要执行的任务添加到任务触发线程池中;
- 任务触发器由快慢线程池组成,根据任务触发时间来进行切换选择由哪一个线程池触发任务。任务触发器根据任务信息组装触发参数(包括基本信息和阻塞策略),任务触发器根据任务配置的路由策略进行路由寻址,然后通过远程调用进行任务触发。
初始化
首先找到配置类 XxlJobAdminConfig
。该类实现InitializingBean
接口和DisposableBean
接口,主要用于xxl-job-admin
初始化和销毁动作。
afterPropertiesSet
执行初始化操作:
java
/**
* 在Bean对象属性填充完成后调用
*/
@Override
public void afterPropertiesSet() throws Exception {
// 利用静态声明的只会加载一次的特性,初始化一个单例对象。
adminConfig = this;
// 初始化xxl-job调度器
xxlJobScheduler = new XxlJobScheduler();
xxlJobScheduler.init();
}
com.xxl.job.admin.core.scheduler.XxlJobScheduler#init
初始化xxl-job
调度器:
java
public void init() throws Exception {
// init i18n
initI18n();
// admin trigger pool start --> 初始化触发器线程池
JobTriggerPoolHelper.toStart();
// admin registry monitor run --> 30秒执行一次,维护注册表信息,判断在线超时时间90s RegistryConfig类中配置
JobRegistryHelper.getInstance().start();
// admin fail-monitor run --> 运行失败监视器,主要失败发送邮箱,重试触发器
JobFailMonitorHelper.getInstance().start();
// admin lose-monitor run ( depend on JobTriggerPoolHelper ) --> 任务结果处理,包括执行器正常回调和任务结果丢失处理
// 调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败;
JobCompleteHelper.getInstance().start();
// admin log report start --> 统计一些失败成功报表
JobLogReportHelper.getInstance().start();
// start-schedule ( depend on JobTriggerPoolHelper ) --> 执行调度器
JobScheduleHelper.getInstance().start();
logger.info(">>>>>>>>> init xxl-job admin success.");
}
该方法主要做了如下事情:
- init i18n
- 初始化触发器线程池
- 维护注册表信息(30秒执行一次),保持心跳
- 将丢失主机信息调度日志更改状态
- 统计一些失败成功报表,删除过期日志
- 执行调度器
具体流程
I. 初始化i18n
主要是针对ExecutorBlockStrategyEnum
枚举的title
属性进行国际化赋值处理
java
private void initI18n() {
// 枚举都是单例的,初始化调用一次赋值后即可
for (ExecutorBlockStrategyEnum item : ExecutorBlockStrategyEnum.values()) {
// SERIAL_EXECUTION=单机串行
// DISCARD_LATER=丢弃后续调度
// COVER_EARLY=覆盖之前调度
item.setTitle(I18nUtil.getString("jobconf_block_".concat(item.name())));
}
}
II. 初始化触发器线程池【JobTriggerPoolHelper
快慢线程池】
JobTriggerPoolHelper
主要维护了两个线程池。
主要由JobTriggerPoolHelper
类完成触发器线程池的初始化
java
/**
* 初始化
* 调度器启动时,初始化了两个线程池,除了慢线程池的队列大一些以及最大线程数由用户自定义以外,其他配置都一致。
* 快线程池用于处理时间短的任务,慢线程池用于处理时间长的任务
*/
public void start() {
// 核心线程数10,最大线程数来自配置,存活时间为60s,队列大小1000,线程工厂配置线程名。拒绝策略为AbortPolicy,直接抛出异常
fastTriggerPool = new ThreadPoolExecutor(10,
XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax(),
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
r -> new Thread(r, "xxl-job, admin JobTriggerPoolHelper-fastTriggerPool-" + r.hashCode()));
// 慢线程池初始化 触发的任务在一分钟内超时10次,则采用慢触发器执行。拒绝策略为AbortPolicy,直接抛出异常
slowTriggerPool = new ThreadPoolExecutor(10,
XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax(),
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000),
r -> new Thread(r, "xxl-job, admin JobTriggerPoolHelper-slowTriggerPool-" + r.hashCode()));
}
注意:这里分别初始化了2个线程池,一个快一个慢,优先选择快,当一分钟以内任务触发时间超时10次【超时时间为:500ms】,则加入慢线程池执行。
III. 维护注册表信息【JobRegistryHelper
】(30秒执行一次)
JobRegistryHelper#start
主要完成3
件事情:
- 初始化注册或者删除线程池,主要负责客户端 注册或者销毁到
xxl_job_registry
表异步处理,调度中心的api
为com.xxl.job.admin.controller.JobApiController
- 初始化守护线程,每
30
秒执行一次。- 从
xxl_job_registry
中删除超时的机器 - 更新
xxl_job_group
执行器地址列表
- 从
java
/**
* 初始化
*/
public void start() {
// for registry or remove --> 注册或者删除线程池初始化,拒绝策略是由父线程执行,同时会打印日志
registryOrRemoveThreadPool = new ThreadPoolExecutor(
2,
10,
30L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(2000),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "xxl-job, admin JobRegistryMonitorHelper-registryOrRemoveThreadPool-" + r.hashCode());
}
},
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 注意:这里是父线程执行任务
r.run();
logger.warn(">>>>>>>>>>> xxl-job, registry or remove too fast, match threadpool rejected handler(run now).");
}
});
// for monitor --> 注册监控线程 30秒【sleep】执行一次,维护注册表信息, 判断在线超时时间90s
registryMonitorThread = new Thread(new Runnable() {
@Override
public void run() {
while (!toStop) {
try {
// auto registry group --> 查询任务组数据。对应xxl-job-group表,有数据时校验自动任务执行器注册信息
List<XxlJobGroup> groupList = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0);
if (groupList != null && !groupList.isEmpty()) {
// remove dead address (admin/executor) --> 从xxl-job-registry中删除超时90s的机器,不分是否自动注册
List<Integer> ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date());
if (ids != null && ids.size() > 0) {
// 移除超时掉线的执行器。执行器的更新时间通过com.xxl.job.core.biz.AdminBiz.registry完成,也就是执行器和admin之间的心跳
XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids);
}
// fresh online address (admin/executor) --> 从xxl-job-registry中获取执行器地址,刷新到xxl-job-group中。刷新在线地址 包含执行器注册的和admin
HashMap<String, List<String>> appAddressMap = new HashMap<>();
// 查询更新时间大于当前时间-90s的数据
List<XxlJobRegistry> list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findAll(RegistryConfig.DEAD_TIMEOUT, new Date());
if (list != null) {
for (XxlJobRegistry item : list) {
if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) {
// group表的appname对应registry表的registrykey字段
String appname = item.getRegistryKey();
List<String> registryList = appAddressMap.get(appname);
if (registryList == null) {
registryList = new ArrayList<String>();
}
if (!registryList.contains(item.getRegistryValue())) {
registryList.add(item.getRegistryValue());
}
appAddressMap.put(appname, registryList);
}
}
}
// fresh group address
for (XxlJobGroup group : groupList) {
List<String> registryList = appAddressMap.get(group.getAppname());
String addressListStr = null;
if (registryList != null && !registryList.isEmpty()) {
// 对地址进行排序
Collections.sort(registryList);
// 用逗号分隔 http:127.0.0.1:9092/,http://127.0.0.1:9903/
StringBuilder addressListSB = new StringBuilder();
for (String item : registryList) {
addressListSB.append(item).append(",");
}
addressListStr = addressListSB.toString();
addressListStr = addressListStr.substring(0, addressListStr.length() - 1);
}
group.setAddressList(addressListStr);
group.setUpdateTime(new Date());
// 更新xxl-job-group中的数据。注册信息中没有数据也会执行更新,将执行器地址更新为空
XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group);
}
}
} catch (Exception e) {
if (!toStop) {
logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:", e);
}
}
try {
// 30s执行一次,通过sleep实现
TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
} catch (InterruptedException e) {
if (!toStop) {
logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:", e);
}
}
}
logger.info(">>>>>>>>>>> xxl-job, job registry monitor thread stop");
}
});
// 守护线程
registryMonitorThread.setDaemon(true);
registryMonitorThread.setName("xxl-job, admin JobRegistryMonitorHelper-registryMonitorThread");
registryMonitorThread.start();
}
IV. 运行失败监视器【JobFailMonitorHelper】失败重试,告警邮件
JobFailMonitorHelper
主要是失败任务重试,以及告警消息发送
- 失败重试
这里判断失败有2种情况(trigger_code
表示任务触发状态,handle_code
表示任务执行结果状态,200
均表示成功,500
表示失败)
第一种:trigger_code!=(0,200) 且 handle_code!=0
第二种:handle_code!=200
- 告警(这里可向
spring
注入JobAlarm
),可自定义扩展
JobFailMonitorHelper
内部初始化了一个守护线程monitorThread
用于检测失败任务,并根据配置的重试规则进行重试和告警。
java
/**
* 初始化任务失败监听类
* <p>
* 线程每10秒执行1次
*/
public void start() {
monitorThread = new Thread(new Runnable() {
@Override
public void run() {
// monitor
while (!toStop) {
try {
// 查询 1000 条失败任务
List<Long> failLogIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findFailJobLogIds(1000);
if (CollUtil.isNotEmpty(failLogIds)) {
for (long failLogId : failLogIds) {
// lock log --> 加锁,乐观修锁改alarm_status=-1
int lockRet = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, 0, -1);
if (lockRet < 1) {
continue;
}
XxlJobLog log = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().load(failLogId);
XxlJobInfo info = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(log.getJobId());
// 1、fail retry monitor 失败重试
if (log.getExecutorFailRetryCount() > 0) {
// 执行重新触发操作
JobTriggerPoolHelper.trigger(log.getJobId(), TriggerTypeEnum.RETRY,
(log.getExecutorFailRetryCount() - 1), log.getExecutorShardingParam(), log.getExecutorParam(), null);
// 追加日志
String retryMsg = "<br><br><span style=\"color:#F39C12;\" > >>>>>>>>>>>" + I18nUtil.getString("jobconf_trigger_type_retry") + "<<<<<<<<<<< </span><br>";
log.setTriggerMsg(log.getTriggerMsg() + retryMsg);
XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateTriggerInfo(log);
}
// 2、fail alarm monitor 任务失败就告警
int newAlarmStatus = 0; // 告警状态:0-默认、-1=锁定状态、1-无需告警、2-告警成功、3-告警失败
if (info != null) {
// 发送告警,并获取发生送结果
boolean alarmResult = XxlJobAdminConfig.getAdminConfig().getJobAlarmer().alarm(info, log);
logger.debug(">>>>>>>> xxl-job 任务执行失败,发送告警信息:jobId:{},重试次数:{}", info.getId(), log.getExecutorFailRetryCount());
newAlarmStatus = alarmResult ? 2 : 3;
} else {
newAlarmStatus = 1;
}
XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateAlarmStatus(failLogId, -1, newAlarmStatus);
}
}
} catch (Exception e) {
if (!toStop) {
logger.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:", e);
}
}
try {
// 10秒执行一次
TimeUnit.SECONDS.sleep(10);
} catch (Exception e) {
if (!toStop) {
logger.error(e.getMessage(), e);
}
}
}
logger.info(">>>>>>>>>>> xxl-job, job fail monitor thread stop");
}
});
// 守护线程
monitorThread.setDaemon(true);
monitorThread.setName("xxl-job, admin JobFailMonitorHelper");
monitorThread.start();
}
其中XxlJobAdminConfig.getAdminConfig().getJobAlarmer().alarm(info, log);
告警的发送,可以实现自定义。即实现JobAlarm
接口,并注入Spring
即可。
java
@Component
public class JobAlarmer implements ApplicationContextAware, InitializingBean {
private static Logger logger = LoggerFactory.getLogger(JobAlarmer.class);
private ApplicationContext applicationContext;
private List<JobAlarm> jobAlarmList;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
Map<String, JobAlarm> serviceBeanMap = applicationContext.getBeansOfType(JobAlarm.class);
if (MapUtil.isNotEmpty(serviceBeanMap)) {
jobAlarmList = new ArrayList<>(serviceBeanMap.values());
}
}
/**
* job alarm
*
* @param info 任务信息
* @param jobLog 任务日志
* @return 告警结果
*/
public boolean alarm(XxlJobInfo info, XxlJobLog jobLog) {
boolean result = false;
if (jobAlarmList != null && jobAlarmList.size() > 0) {
result = true; // success means all-success
for (JobAlarm alarm : jobAlarmList) {
boolean resultItem = false;
try {
resultItem = alarm.doAlarm(info, jobLog);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
if (!resultItem) {
result = false;
}
}
}
return result;
}
}
V. 任务结果处理【JobCompleteHelper】
主要职责:
- 初始化线程池和守护线程
- 守护线程每60秒执行一次,将执行器客户端失联的任务状态标记为完成【两个条件:a.超过10分钟都处于运行中;b.失联】
- 线程池主要用于异步处理执行器的任务结果回调
java
/**
* 初始化
*/
public void start() {
// for callback --> 回调线程
callbackThreadPool = new ThreadPoolExecutor(
2,
20,
30L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3000),
r -> new Thread(r, "xxl-job, admin JobLosedMonitorHelper-callbackThreadPool-" + r.hashCode()),
(r, executor) -> {
// 超过最大数量后,父线程执行任务
r.run();
log.warn(">>>>>>>>>>> xxl-job, callback too fast, match threadpool rejected handler(run now).");
});
// for monitor -> 监听线程。每60秒执行一次
monitorThread = new Thread(new Runnable() {
@Override
public void run() {
// wait for JobTriggerPoolHelper-init
try {
// 首次运行,暂停50毫秒,目的是为了让JobTriggerPoolHelper先初始化完成
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
if (!toStop) {
log.error(e.getMessage(), e);
}
}
// monitor --> 监听
while (!toStop) {
try {
// 任务结果丢失处理:调度记录停留在 "运行中" 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败;
// 两个条件:1.运行中状态超过10min 2.心跳不在线
Date losedTime = DateUtil.addMinutes(new Date(), -10);
List<Long> losedJobIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLostJobIds(losedTime);
if (CollUtil.isNotEmpty(losedJobIds)) {
for (Long logId : losedJobIds) {
XxlJobLog jobLog = new XxlJobLog();
jobLog.setId(logId);
jobLog.setHandleTime(new Date());
jobLog.setHandleCode(ReturnT.FAIL_CODE);
// 任务结果丢失,标记失败
jobLog.setHandleMsg(I18nUtil.getString("joblog_lost_fail"));
XxlJobCompleter.updateHandleInfoAndFinish(jobLog);
}
}
} catch (Exception e) {
if (!toStop) {
log.error(">>>>>>>>>>> xxl-job, job fail monitor thread error:", e);
}
}
try {
// 每60秒执行一次
TimeUnit.SECONDS.sleep(60);
} catch (Exception e) {
if (!toStop) {
log.error(e.getMessage(), e);
}
}
}
log.info(">>>>>>>>>>> xxl-job, JobLosedMonitorHelper stop");
}
});
// 守护线程
monitorThread.setDaemon(true);
monitorThread.setName("xxl-job, admin JobLosedMonitorHelper");
monitorThread.start();
}
com.xxl.job.admin.core.complete.XxlJobCompleter#updateHandleInfoAndFinish
处理任务结果,有子任务触发子任务
java
/**
* 任务结果刷新入口
* common fresh handle entrance (limit only once)
*
* @param xxlJobLog 任务信息
*/
public static int updateHandleInfoAndFinish(XxlJobLog xxlJobLog) {
// finish 处理任务结果,有子任务执行子任务
finishJob(xxlJobLog);
// text最大64kb 避免长度过长
if (xxlJobLog.getHandleMsg().length() > 15000) {
xxlJobLog.setHandleMsg(xxlJobLog.getHandleMsg().substring(0, 15000));
}
// fresh handle
return XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().updateHandleInfo(xxlJobLog);
}
VI. 报表统计与日志清理【JobLogReportHelper】
- 按天统计报表数据(
xxl_job_log_report
)1分钟执行一次 - 定时清理日志信息(
xxl_job_log
)24小时执行一次
java
/**
* 初始化,启动一个守护线程处理任务报表
* 每分钟执行一次
*/
public void start() {
// 每一分钟执行一次
logrThread = new Thread(() -> {
// 上次清理日志时间
long lastCleanLogTime = 0;
while (!toStop) {
// 1、log-report refresh: refresh log report in 3 days
try {
// 分别统计今天,昨天,前天0~24点的数据 每天开始时间为 00:00:00.000 结束时间为:23:59:59.999
for (int i = 0; i < 3; i++) {
// 获取当前迁移i天的开始时间数据。
Calendar itemDay = Calendar.getInstance();
itemDay.add(Calendar.DAY_OF_MONTH, -i);
itemDay.set(Calendar.HOUR_OF_DAY, 0);
itemDay.set(Calendar.MINUTE, 0);
itemDay.set(Calendar.SECOND, 0);
itemDay.set(Calendar.MILLISECOND, 0);
// 开始时间,getTime() 是通过new Date()返回的。
Date todayFrom = itemDay.getTime();
itemDay.set(Calendar.HOUR_OF_DAY, 23);
itemDay.set(Calendar.MINUTE, 59);
itemDay.set(Calendar.SECOND, 59);
itemDay.set(Calendar.MILLISECOND, 999);
// 结束时间
Date todayTo = itemDay.getTime();
XxlJobLogReport xxlJobLogReport = new XxlJobLogReport();
xxlJobLogReport.setTriggerDay(todayFrom);
xxlJobLogReport.setRunningCount(0);
xxlJobLogReport.setSucCount(0);
xxlJobLogReport.setFailCount(0);
// 查询当前数据 开始时间为 00:00:00.000 结束时间为:23:59:59.999
Map<String, Object> triggerCountMap = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findLogReport(todayFrom, todayTo);
if (MapUtil.isNotEmpty(triggerCountMap)) {
// 触发总数
int triggerDayCount = Integer.parseInt(String.valueOf(triggerCountMap.getOrDefault("triggerDayCount", 0)));
// 运行中 trigger_code in (0, 200) and handle_code = 0
int triggerDayCountRunning = Integer.parseInt(String.valueOf(triggerCountMap.getOrDefault("triggerDayCountRunning", 0)));
// 成功 handle_code = 200
int triggerDayCountSuc = Integer.parseInt(String.valueOf(triggerCountMap.getOrDefault("triggerDayCountSuc", 0)));
// 失败数据
int triggerDayCountFail = triggerDayCount - triggerDayCountRunning - triggerDayCountSuc;
xxlJobLogReport.setRunningCount(triggerDayCountRunning);
xxlJobLogReport.setSucCount(triggerDayCountSuc);
xxlJobLogReport.setFailCount(triggerDayCountFail);
}
// do refresh 先执行更新,无数据才插入,能在一定程度上解决调度器执行器多节点并发问题
// 旧数据执行更新,新数据执行保存。更新返回的是变动行数,小于1则表示库里不存在 。根据报表时间更新数据
int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().update(xxlJobLogReport);
if (ret < 1) {
// 这里还是有很小的可能会同时执行到,导致数据有多份的情况
XxlJobAdminConfig.getAdminConfig().getXxlJobLogReportDao().save(xxlJobLogReport);
}
}
} catch (Exception e) {
if (!toStop) {
log.error(">>>>>>>>>>> xxl-job, job log report thread error:", e);
}
}
// 2、log-clean: switch open & once each day 开关打卡并且每24小时执行一次
// 设置了保留日志天数,并且有效时(小于7为-1),距离上次清理超过24小时
if (XxlJobAdminConfig.getAdminConfig().getLogretentiondays() > 0
&& System.currentTimeMillis() - lastCleanLogTime > 24 * 60 * 60 * 1000) {
// expire-time 获取开始清理时间。例如配置了7天,今天是2023-08-12 那么clearBeforeTime就是2023-08-05 00:00:00.000
Calendar expiredDay = Calendar.getInstance();
expiredDay.add(Calendar.DAY_OF_MONTH, -1 * XxlJobAdminConfig.getAdminConfig().getLogretentiondays());
expiredDay.set(Calendar.HOUR_OF_DAY, 0);
expiredDay.set(Calendar.MINUTE, 0);
expiredDay.set(Calendar.SECOND, 0);
expiredDay.set(Calendar.MILLISECOND, 0);
Date clearBeforeTime = expiredDay.getTime();
// clean expired log
List<Long> logIds;
do {
// 每次1000条 执行清理,mysql in最多1000个
logIds = XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().findClearLogIds(0, 0, clearBeforeTime, 0, 1000);
if (CollUtil.isNotEmpty(logIds)) {
XxlJobAdminConfig.getAdminConfig().getXxlJobLogDao().clearLog(logIds);
}
} while (CollUtil.isNotEmpty(logIds));
// update clean time
lastCleanLogTime = System.currentTimeMillis();
}
try {
// 每1分钟钟执行一次
TimeUnit.MINUTES.sleep(1);
} catch (Exception e) {
if (!toStop) {
log.error(e.getMessage(), e);
}
}
}
log.info(">>>>>>>>>>> xxl-job, job log report thread stop");
});
logrThread.setDaemon(true);
logrThread.setName("xxl-job, admin JobLogReportHelper");
logrThread.start();
}
VII. 执行调度器【JobScheduleHelper】(核心)
执行调度器主要由包含了两个线程。一个线程(scheduleThread
)负责加锁查询任务信息,对任务按照触发时间分类,并按照具体策略执行或者计算下次调度时间。对于执行时间间隔非常短的任务会根据具体的策略放入时间轮,然后由另一个线程(ringThread
)进行任务触发处理。
scheduleThread
执行周期:
- 扫描超时(大于1000ms),不等待,直接继续执行。
- 预读数据不为空,执行周期为:0-1000ms。预读数据为空,执行周期为4000-5000ms
java
// Wait seconds, align second 耗时小于1秒,-->数据少。可以sleep一会。数据多的情况下。一直执行
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) {
log.error(e.getMessage(), e);
}
}
}
scheduleThread
会加锁查询出下次执行时间在未来5秒以内的所有任务,默认一次最多获取6000条。然后根据过期时间会分成三种对应处理。
- 触发器下次执行时间过期时间 > 5S
- 触发器下次执行时间过期时间 < 5S
- 触发器下次执行时间在未来5S以内。
ringThread
主要处理时间轮中的定时任务,执行周期为:0-1000ms。
时间轮出自Netty
中的HashedWheelTimer
,是一个环形结构,可以用时钟来类比,钟面上有很多bucket
,每一个bucket
上可以存放多个任务,使用一个List
保存该时刻到期的所有任务,同时一个指针随着时间流逝一格一格转动,并执行对应bucket
上所有到期的任务。任务通过取模决定应该放入哪个bucket
。和HashMap
的原理类似,newTask
对应put
,使用List
来解决 Hash
冲突。
xxl-job
中一个时间轮有60个bucket
,从0-59
。用于存储当前秒执行的任务列表。
以上图为例,假设一个bucket
是1秒,则指针转动一轮表示的时间段为60s
,假设当前指针指向0
,此时需要调度一个3s
后执行的任务,显然应该加入到(0+3=3)
的方格中,指针再走3s
次就可以执行了;
具体代码如下:
java
package com.xxl.job.admin.core.thread;
import com.xxl.job.admin.core.conf.XxlJobAdminConfig;
import com.xxl.job.admin.core.cron.CronExpression;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum;
import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum;
import com.xxl.job.admin.core.trigger.TriggerTypeEnum;
import com.xxl.job.admin.core.util.CollUtil;
import lombok.extern.slf4j.Slf4j;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* 任务执行调度
* <p>
* 工作流程:
* 周期性的遍历所有的jobInfo这个表,通过数据库的行锁和事务一致性,通过for update 来保证多个调度中心集群在同一时间内只有一个调度中心在调度任务
* <p>
* 周期性的遍历所有的jobInfo这个表,读取触发时间小于nowtime+5s这个时间之前的所有任务,然后进行引入以下触发机制判断
* <p>
* 三种触发任务机制:
* <ol>
* <li>nowtime-TriggerNextTime()>PRE_READ_MS(5s) 既超过有效误差内【5秒内】,则查看当前任务的失效调度策略,若为立即重试一次,则立即触发调度任务,且触发类型为misfire</li>
* <li>nowtime-TriggerNextTime()<PRE_READ_MS(5s) 既没有超过有效误差【过5秒】,则立即调度调度任务</li>
* <li>nowtime<TriggerNextTime() 则说明这个任务马上就要触发了,放到一个时间轮上(https://blog.csdn.net/zalu9810/article/details/113396131),</li>
* </ol>
* <p>
* 随后将快要触发的任务放到时间轮上,时间轮由key(将要触发的时间s),value(在当前触发s的所有任务id集合),然后更新这个任务的下一次触发时间
* <p>
* 这个时间轮的任务遍历交由第二个线程处理ringThread,周期在1s之内周期的扫描这个时间轮,然后执行调度任务
*
* @author xuxueli 2019-05-21
*/
@Slf4j
public class JobScheduleHelper {
private static JobScheduleHelper instance = new JobScheduleHelper();
public static JobScheduleHelper getInstance() {
return instance;
}
/**
* 预读误差时间,5秒
*/
public static final long PRE_READ_MS = 5000;
/**
* 调度线程,执行周期:【0-1000ms】、【4000-5000ms】内的随时时间执行
*/
private Thread scheduleThread;
/**
* 时间轮线程,主要处理ringData中的任务数据。并触发任务。注意这里执行周期 0-1000ms
*/
private Thread ringThread;
/**
* 默认调度线程停止标志
*/
private volatile boolean scheduleThreadToStop = false;
/**
* 时间轮线程停止标志
*/
private volatile boolean ringThreadToStop = false;
/**
* 时间轮,环上数据长度为60。即key的范围是0-59秒。value是在具体秒数需要执行的任务ID
*/
private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();
public void start() {
// schedule thread
scheduleThread = new Thread(() -> {
try {
// sleep 4000-5000毫秒,时间返回内随机,避免各调度中心节点同时执行
TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis() % 1000);
} catch (InterruptedException e) {
if (!scheduleThreadToStop) {
log.error(e.getMessage(), e);
}
}
log.info(">>>>>>>>> init xxl-job admin scheduler success.");
// pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20)
// 每个触发器花费50ms,每个线程单位时间(秒)内处理20任务,默认最多同时处理300*20=6000任务
int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20;
while (!scheduleThreadToStop) {
// Scan Job
long start = System.currentTimeMillis();
Connection conn = null;
boolean connAutoCommit = true;
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、pre read
long nowTime = System.currentTimeMillis();
// 查询条件:1. 下次触发时间小于当前时间+5s and 2.triggerStatus为1(调度状态:0-停止,1-运行) and 3. 数据量(默认取值为6000条【根据配置变动】)
// 任务调度错过触发时间时的可能原因:服务重启;调度线程被阻塞,线程被耗尽;上次调度持续阻塞,下次调度被错过;
List<XxlJobInfo> scheduleList = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleJobQuery(nowTime + PRE_READ_MS, preReadCount);
if (CollUtil.isNotEmpty(scheduleList)) {
// 2、push time-ring
for (XxlJobInfo jobInfo : scheduleList) {
// time-ring jump
if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) {
// 2.1、trigger-expire > 5s:pass && make next-trigger-time --> 任务过期超过5秒 当前时间-任务执行时间>5秒 -->按照过期策略处理并刷新下一次触发时间
log.warn(">>>>>>>>>>> xxl-job, schedule misfire, jobId = {}", jobInfo.getId());
// 1、misfire match 过期处理策略-->FIRE_ONCE_NOW:立即执行一次
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);
log.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = {}", jobInfo.getId());
}
// 2、fresh next 刷新下一次执行时间
refreshNextValidTime(jobInfo, new Date());
} else if (nowTime > jobInfo.getTriggerNextTime()) {
// 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time 任务过期小于5秒 --> 直接触发任务并计算下次触发时间
// 1、trigger
JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null, null);
log.debug(">>>>>>>>>>> xxl-job, schedule push trigger : jobId = {}", jobInfo.getId());
// 2、fresh next
refreshNextValidTime(jobInfo, new Date());
// next-trigger-time in 5s, pre-read again 下次触发时间在未来5秒内,这块跟else中逻辑一致,目的是为了避免下次扫描时漏掉数据
if (jobInfo.getTriggerStatus() == 1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) {
// 1、make ring second 时间转化为秒 时间轮为长度为60 (如果执行时间为 2023/08/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 更新任务信息
long currentTime = System.currentTimeMillis();
for (XxlJobInfo jobInfo : scheduleList) {
XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo);
}
log.debug(">>>>>>>>> xxl-job,更新任务信息耗时统计,count:{}, Time-consuming:{}ms", scheduleList.size(), System.currentTimeMillis() - currentTime);
} else {
preReadSuc = false;
}
// tx stop
} catch (Exception e) {
if (!scheduleThreadToStop) {
log.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:", e);
}
} finally {
// commit
if (conn != null) {
try {
// 提交事务
conn.commit();
} catch (SQLException e) {
if (!scheduleThreadToStop) {
log.error(e.getMessage(), e);
}
}
try {
// 设置为自动提交
conn.setAutoCommit(connAutoCommit);
} catch (SQLException e) {
if (!scheduleThreadToStop) {
log.error(e.getMessage(), e);
}
}
try {
// 关闭连接
conn.close();
} catch (SQLException e) {
if (!scheduleThreadToStop) {
log.error(e.getMessage(), e);
}
}
}
// close PreparedStatement
if (null != preparedStatement) {
try {
preparedStatement.close();
} catch (SQLException e) {
if (!scheduleThreadToStop) {
log.error(e.getMessage(), e);
}
}
}
}
long cost = System.currentTimeMillis() - start;
// Wait seconds, align second 耗时小于1秒,-->数据少。可以sleep一会。数据多的情况下。一直执行
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) {
log.error(e.getMessage(), e);
}
}
}
}
log.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread stop");
});
scheduleThread.setDaemon(true);
scheduleThread.setName("xxl-job, admin JobScheduleHelper#scheduleThread");
scheduleThread.start();
// ring thread 时间轮
ringThread = new Thread(new Runnable() {
@Override
public void run() {
while (!ringThreadToStop) {
// align second
try {
// 执行周期 0-1000ms
TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
} catch (InterruptedException e) {
if (!ringThreadToStop) {
log.error(e.getMessage(), e);
}
}
try {
// second data
// 时间轮上的数据集合。即任务ID集合
List<Integer> ringItemData = new ArrayList<>();
// 避免处理耗时太长,跨过刻度,向前校验一个刻度;
int nowSecond = Calendar.getInstance().get(Calendar.SECOND);
for (int i = 0; i < 2; i++) {
// (nowSecond + 60 - i) % 60 和 (nowSecond - i) % 60 加60的目的,避免为负数
List<Integer> tmpData = ringData.remove((nowSecond + 60 - i) % 60);
if (tmpData != null) {
ringItemData.addAll(tmpData);
}
}
// ring trigger
log.debug(">>>>>>>>>>> xxl-job, time-ring beat : {} = {}", nowSecond, Collections.singletonList(ringItemData));
// 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) {
log.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:", e);
}
}
}
log.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
}
});
ringThread.setDaemon(true);
ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
ringThread.start();
}
/**
* 计算任务下一次触发时间
*
* @param jobInfo 任务信息
* @param fromTime 当前时间
* @throws Exception exp
*/
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 {
// 调度状态:0-停止,1-运行
jobInfo.setTriggerStatus(0);
jobInfo.setTriggerLastTime(0);
jobInfo.setTriggerNextTime(0);
log.warn(">>>>>>>>>>> xxl-job, refreshNextValidTime fail for job: jobId={}, scheduleType={}, scheduleConf={}", jobInfo.getId(), jobInfo.getScheduleType(), jobInfo.getScheduleConf());
}
}
/**
* 添加任务到时间轮
*
* @param ringSecond 时间【秒】
* @param jobId 任务id
*/
private void pushTimeRing(int ringSecond, int jobId) {
// push async ring
// 时间轮不存在对应时间时就新建一个list,存在取值。list中添加任务id
List<Integer> ringItemData = ringData.computeIfAbsent(ringSecond, k -> new ArrayList<>());
ringItemData.add(jobId);
log.debug(">>>>>>>>>>> xxl-job, schedule push time-ring : {} = {}", ringSecond, Collections.singletonList(ringItemData));
}
public void toStop() {
// 1、stop schedule
scheduleThreadToStop = true;
try {
TimeUnit.SECONDS.sleep(1); // wait
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
if (scheduleThread.getState() != Thread.State.TERMINATED) {
// interrupt and wait
scheduleThread.interrupt();
try {
scheduleThread.join();
} catch (InterruptedException e) {
log.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) {
log.error(e.getMessage(), e);
}
}
// stop ring (wait job-in-memory stop)
ringThreadToStop = true;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
if (ringThread.getState() != Thread.State.TERMINATED) {
// interrupt and wait
ringThread.interrupt();
try {
ringThread.join();
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
}
log.info(">>>>>>>>>>> xxl-job, JobScheduleHelper stop");
}
// ---------------------- tools ----------------------
/**
* 根据当前时间计算下次执行时间
*
* @param jobInfo 任务信息
* @param fromTime 当前时间
* @return 下次执行时间
* @throws Exception Exp
*/
public static Date generateNextValidTime(XxlJobInfo jobInfo, Date fromTime) throws Exception {
ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null);
if (ScheduleTypeEnum.CRON == scheduleTypeEnum) {
// 返回满足cron表达式的给定日期/时间之后的下一个日期/时间
return new CronExpression(jobInfo.getScheduleConf()).getNextValidTimeAfter(fromTime);
} else if (ScheduleTypeEnum.FIX_RATE == scheduleTypeEnum /*|| ScheduleTypeEnum.FIX_DELAY == scheduleTypeEnum*/) {
// 当前时间之后的下一次时间 固定速率
return new Date(fromTime.getTime() + Integer.parseInt(jobInfo.getScheduleConf()) * 1000L);
}
return null;
}
}
执行器启动流程
执行器启动流程时序图
🧊主要流程:
- 在
XxlJobSpringExecutor
初始化时执行相关的方法;- 解析标有
XxlJob
注解的方法,标有Lazy
的类不处理。将标注有XxlJob
注解的方法转化为MethodJobHandler
类,并存储到XxlJobExecutor#jobHandlerRepository
属性中。- 初始化
SpringGlueFactory
。- 初始化日志路径,
XxlJobFileAppender
主要用于处理日志- 初始化
admin-client
,用于进行任务回调以及心跳检查- 初始化日志清理线程
JobLogFileCleanThread
- 初始化任务回调线程
TriggerCallbackThread
- 启动内嵌服务
EmbedServer
,基于netty
实现
初始化
客户端执行器的核心接口是XxlJobExecutor
,主要有两个实现类,XxlJobSimpleExecutor
和XxlJobSpringExecutor
。其中XxlJobSpringExecutor
主要是针对spring
框架的。
xxl-job
整合Spring
场景下,需要手动配置XxlJobSpringExecutor
实例,并注册为bean
。
java
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
在XxlJobExecutor
接口中主要实现了执行器客户端的启动和销毁、admin-client
远程调用初始化、executor-server
远程调用初始化、JobHandler
任务缓存、jobthread
任务线程缓存。
接口继承体系,XxlJobSpringExecutor
注入了 ApplicationContext
对象。以及实现了 SmartInitializingSingleton
接口,实现该接口的当spring
容器初始完成,紧接着执行监听器发送监听后,就会遍历所有的Bean
然后初始化所有单例非懒加载的bean
,最后在实例化阶段结束时触发回调接口。
com.xxl.job.core.executor.impl.XxlJobSpringExecutor#afterSingletonsInstantiated
主要完成三件事:
- 初始化调度器资源管理器(从
spring
容器中将标记了XxlJob
注解的方法,将其封装并添加到map
中) - 刷新
GlueFactory
- 启动服务,接收服务器请求等
java
// start
@Override
public void afterSingletonsInstantiated() {
// init JobHandler Repository
/*initJobHandlerRepository(applicationContext);*/
// init JobHandler Repository (for method) 初始化任务 标记XxlJob注解的方法类型的
initJobHandlerMethodRepository(applicationContext);
// refresh GlueFactory 舒心GlueFactory
GlueFactory.refreshInstance(1);
// super start 调用父类接口,启动服务
try {
super.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
具体流程
I. 初始化JobHandler
com.xxl.job.core.executor.impl.XxlJobSpringExecutor#initJobHandlerMethodRepository
该方法主要做了如下事情:
- 从
spring
容器获取所有对象,并遍历查找方法上标记XxlJob
注解的方法。 - 将
xxljob
配置的jobname
作为key
,根据初始化和销毁方法配置数据构造MethodJobHandler
作为value
注册jobHandlerRepository
中
任务执行接口IJobHandler
,之前版本是自动注册IJobHandler
接口的实现类的,后续版本改为了注册标记了@XxlJob
注解的方法。如果有IJobHandler
实现类形式,需要自己注册。
com.xxl.job.core.executor.impl.XxlJobSpringExecutor#initJobHandlerMethodRepository
方法比较简单。主要流程:
- 加载所有非懒加载
Bean
; - 找出标记了
XxlJob
注解的方法,并解析初始化和销毁属性,并构造MethodJobHandler
类 - 注册
MethodJobHandler
到jobHandlerRepository
缓存中。MethodJobHandler
任务最终是通过反射调用执行的。
java
private void initJobHandlerMethodRepository(ApplicationContext applicationContext) {
if (applicationContext == null) {
return;
}
// init job handler from method
String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class, false, true);
for (String beanDefinitionName : beanDefinitionNames) {
// get bean
Object bean = null;
Lazy onBean = applicationContext.findAnnotationOnBean(beanDefinitionName, Lazy.class);
if (onBean!=null){
logger.debug("xxl-job annotation scan, skip @Lazy Bean:{}", beanDefinitionName);
continue;
}else {
bean = applicationContext.getBean(beanDefinitionName);
}
// filter method
Map<Method, XxlJob> annotatedMethods = null; // referred to :org.springframework.context.event.EventListenerMethodProcessor.processBean
try {
annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
new MethodIntrospector.MetadataLookup<XxlJob>() {
@Override
public XxlJob inspect(Method method) {
return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
}
});
} catch (Throwable ex) {
logger.error("xxl-job method-jobhandler resolve error for bean[" + beanDefinitionName + "].", ex);
}
if (annotatedMethods==null || annotatedMethods.isEmpty()) {
continue;
}
// generate and regist method job handler
for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
Method executeMethod = methodXxlJobEntry.getKey();
XxlJob xxlJob = methodXxlJobEntry.getValue();
// regist
registJobHandler(xxlJob, bean, executeMethod);
}
}
}
com.xxl.job.core.executor.XxlJobExecutor#registJobHandler(com.xxl.job.core.handler.annotation.XxlJob, java.lang.Object, java.lang.reflect.Method)
方法完成注册
java
protected void registJobHandler(XxlJob xxlJob, Object bean, Method executeMethod){
if (xxlJob == null) {
return;
}
String name = xxlJob.value();
//make and simplify the variables since they'll be called several times later
Class<?> clazz = bean.getClass();
String methodName = executeMethod.getName();
if (name.trim().length() == 0) {
throw new RuntimeException("xxl-job method-jobhandler name invalid, for[" + clazz + "#" + methodName + "] .");
}
if (loadJobHandler(name) != null) {
throw new RuntimeException("xxl-job jobhandler[" + name + "] naming conflicts.");
}
// execute method
/*if (!(method.getParameterTypes().length == 1 && method.getParameterTypes()[0].isAssignableFrom(String.class))) {
throw new RuntimeException("xxl-job method-jobhandler param-classtype invalid, for[" + bean.getClass() + "#" + method.getName() + "] , " +
"The correct method format like \" public ReturnT<String> execute(String param) \" .");
}
if (!method.getReturnType().isAssignableFrom(ReturnT.class)) {
throw new RuntimeException("xxl-job method-jobhandler return-classtype invalid, for[" + bean.getClass() + "#" + method.getName() + "] , " +
"The correct method format like \" public ReturnT<String> execute(String param) \" .");
}*/
executeMethod.setAccessible(true);
// init and destroy
Method initMethod = null;
Method destroyMethod = null;
if (xxlJob.init().trim().length() > 0) {
try {
initMethod = clazz.getDeclaredMethod(xxlJob.init());
initMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException("xxl-job method-jobhandler initMethod invalid, for[" + clazz + "#" + methodName + "] .");
}
}
if (xxlJob.destroy().trim().length() > 0) {
try {
destroyMethod = clazz.getDeclaredMethod(xxlJob.destroy());
destroyMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException("xxl-job method-jobhandler destroyMethod invalid, for[" + clazz + "#" + methodName + "] .");
}
}
// registry jobhandler
registJobHandler(name, new MethodJobHandler(bean, executeMethod, initMethod, destroyMethod));
}
II. 刷新GlueFactory
com.xxl.job.core.glue.GlueFactory#refreshInstance
刷新GlueFactory
,工厂模式
java
public static void refreshInstance(int type){
if (type == 0) {
glueFactory = new GlueFactory();
} else if (type == 1) {
glueFactory = new SpringGlueFactory();
}
}
III. 核心启动类【XxlJobExecutor】
该方法主要做了如下事情:
- 初始化日志文件
- 封装调度中心请求路径,用于访问调度中心
- 清除过期日志
- 回调调度中心任务执行状态
- 执行内嵌服务
com.xxl.job.core.executor.XxlJobExecutor#start
方法
java
public void start() throws Exception {
// init logpath 日志路径初始化
XxlJobFileAppender.initLogPath(logPath);
// init invoker, admin-client admin-client初始化,
initAdminBizList(adminAddresses, accessToken);
// init JobLogFileCleanThread
JobLogFileCleanThread.getInstance().start(logRetentionDays);
// init TriggerCallbackThread
TriggerCallbackThread.getInstance().start();
// init executor-server
initEmbedServer(address, ip, port, appname, accessToken);
}
初始化日志文件【XxlJobFileAppender】
XxlJobFileAppender
主要用于处理执行日志信息。包括日志路径初始化、创建日志文件、追加日志、读取日志信息等。
方法都比较简单,这里不过多介绍。
初始化调度中心客户端【AdminBizClient】
AdminBizClient
封装调度中心请求路径,用于访问调度中心。
java
private void initAdminBizList(String adminAddresses, String accessToken) throws Exception {
if (adminAddresses!=null && adminAddresses.trim().length()>0) {
for (String address: adminAddresses.trim().split(",")) {
if (address!=null && address.trim().length()>0) {
AdminBiz adminBiz = new AdminBizClient(address.trim(), accessToken);
if (adminBizList == null) {
adminBizList = new ArrayList<AdminBiz>();
}
adminBizList.add(adminBiz);
}
}
}
}
执行器日志文件清理【JobLogFileCleanThread】
JobLogFileCleanThread
日志文件清理线程,主要用于日志文件清理。需要注意的是:配置参数小于3天不执行清理。每天执行一次清理。
代码也非常简单
java
public void start(final long logRetentionDays) {
// limit min value
if (logRetentionDays < 3) {
return;
}
localThread = new Thread(new Runnable() {
@Override
public void run() {
while (!toStop) {
try {
// clean log dir, over logRetentionDays
File[] childDirs = new File(XxlJobFileAppender.getLogPath()).listFiles();
if (childDirs != null && childDirs.length > 0) {
// today
Calendar todayCal = Calendar.getInstance();
todayCal.set(Calendar.HOUR_OF_DAY, 0);
todayCal.set(Calendar.MINUTE, 0);
todayCal.set(Calendar.SECOND, 0);
todayCal.set(Calendar.MILLISECOND, 0);
Date todayDate = todayCal.getTime();
for (File childFile : childDirs) {
// valid
if (!childFile.isDirectory()) {
continue;
}
if (childFile.getName().indexOf("-") == -1) {
continue;
}
// file create date
Date logFileCreateDate = null;
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
logFileCreateDate = simpleDateFormat.parse(childFile.getName());
} catch (ParseException e) {
logger.error(e.getMessage(), e);
}
if (logFileCreateDate == null) {
continue;
}
if ((todayDate.getTime() - logFileCreateDate.getTime()) >= logRetentionDays * (24 * 60 * 60 * 1000)) {
FileUtil.deleteRecursively(childFile);
}
}
}
} catch (Exception e) {
if (!toStop) {
logger.error(e.getMessage(), e);
}
}
try {
// 每天执行一次
TimeUnit.DAYS.sleep(1);
} catch (InterruptedException e) {
if (!toStop) {
logger.error(e.getMessage(), e);
}
}
}
logger.info(">>>>>>>>>>> xxl-job, executor JobLogFileCleanThread thread destroy.");
}
});
localThread.setDaemon(true);
localThread.setName("xxl-job, executor JobLogFileCleanThread");
localThread.start();
}
回调调度中心反馈任务结果【TriggerCallbackThread】
TriggerCallbackThread
主要用于处理任务回调,以及任务回调失败后的重试操作。
服务注册与心跳检测
服务注册主要是指执行器客户端每隔30s
向调度中心定时发送执行器配置信息(appName
、address
)等,在执行器中主要通过ExecutorRegistryThread
类来完成。注册过程通过调用调度中心的api
接口来完成注册信息传递。
在调度中心也会检测执行器是否失联(超过90s
未上报数据),失联的执行器地址会被清理。
主要的核心类包括:
- 执行器客户端:
ExecutorRegistryThread
执行器注册线程,每隔30s
向调度中心注册一次。通过AdminBizClient
发送出注册请求,都是post
请求。- 调度中心:
AdminBizImpl
接收到请求不出特殊处理,转交给JobRegistryHelper
完成注册JobRegistryHelper
内部维护了registryOrRemoveThreadPool
注册或者移除线程池,用于异步处理客户端的注册请求。JobRegistryHelper
内部还维护了registryMonitorThread
监控线程,用于处理超过90s
未进行注册更新的执行器,每30s
处理一次。