初始化动作
客户端初始化:
客户端重要对象(可先大致看一下,在代码中碰到对应的对象时,在来看)
对象 | 描述 | 特点 |
JobHandler | 任务执行器 | 具体需要调度的方法(被@Xxl标解的方法) |
JobThread | 任务执行线程(内部维护有队例) | 每个任务都有对应的执行线程,如果在90s之内重复执行相同任务,则使用的是相同线程,反之,超过90秒未执行就会卸载工作线程JobThread (可基于实际场景优化) |
ExecutorRegistryThread | 执行器的心跳与注册线程 | 每30s运行一次调用RPC访问所有维护的服务端(可基于实际场景优化) |
GlueType | 运行模式,常见有Bean,Glue(Java),Glue(Shell)等等 | 常用一般都是bean模式(使用JobHander),不过XXL也支持直接使用脚本执行(Runtime.getRuntime().exec),本文主要讲的也是Bean模式 |
AdminBizClient | 调用服务端对应的客户端类 | 一个服务器端对应一个Client,内部封装有 : 1.执行结果回调服务端 2.向服务端注册当前客户端 3.取消注册 三个接口 |
adminAddress | 服务端地址 | |
address | 客户端地址 | 可以为空,为空使用本地IP加指定port |
ip | 客户端的地址.默认为本机 | 可以为空,为空则取address,或者会自动获取本机地址 |
port | 客户端的端口,默认9999 | 可以为空,为空会自动在0-65535中寻找可用端口,默认9999(先寻找9999-65535区间,在寻找9998-0区间) |
XxlJobLogger |
配置xxlJobSpringExecutor
XxlJobSpringExecutor 实现了 SmartInitializingSingleton
里面有一个afterSingletonsInstantiated方法,在XxlJobSpringExecutor创建好bean之后会调用此方法
// start
@Override
public void afterSingletonsInstantiated() {
// 初始化JobHandler ,添加了@XxlJob注解的Bean包装成JobHandler,放到Map中包括
// private static ConcurrentMap<String, IJobHandler> jobHandlerRepository = new ConcurrentHashMap<String, IJobHandler>();
// Key就是@XxlJob所设置的 name,value就是对应的执行方法(包括init方法和destory方法)封装为一个JOBHandler
initJobHandlerMethodRepository(applicationContext);
// 刷新 GlueFactory,使用SpringGlueFactory实现
// 主要用于GLUE模式执行脚本,底层使用的是Runtime.getRuntime().exec
GlueFactory.refreshInstance(1);
// super start
try {
// 调用XxlJobExecutor
super.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// ---------------------- start + stop ----------------------
public void start() throws Exception {
// init logpath
// 初始化日志地址(内置有默认的地址(/data/applogs/xxl-job/jobhandler/gluesource),如果自定义了地址,则地址+/gluesource)
XxlJobFileAppender.initLogPath(logPath);
// init invoker, admin-client,
// 加载服务器地址到list(adminBizList)中,服务端可以多机部署,每一个服务端则创建一个对应的AdminBizClient客户端
initAdminBizList(adminAddresses, accessToken);
// init JobLogFileCleanThread
// 日志清理线程logRetentionDays 配置少于3则不进行清理
JobLogFileCleanThread.getInstance().start(logRetentionDays);
// init TriggerCallbackThread
// 回调线程初始化,简单的来说就是把执行结果同步给服务端
TriggerCallbackThread.getInstance().start();
// init executor-server
// 初始化RPC相关功能, 端口默认9999 与服务器建立连接
// 同时与注册的所有服务器进行注册和心跳,每30S心跳一次
initEmbedServer(address, ip, port, appname, accessToken);
}
服务端:
重要对象功能说明
对象 | 描述 | 特点 |
JobAlarm | 告警接口 | 可自定义扩展告警信息,如钉钉告警,默认为邮件告警,维护了邮件则会触发所有的告警机制。 |
XxlJobAdminConfig 配置字段说明
字段 | 含义 | 默认值 | 备注 |
i18n | 国服化 | zh_CN | |
accessToken | token,与客户端 配置一致 | ||
triggerPoolFastMax | 调度线程池快任务线程大小 | 200 | 最小为200 |
triggerPoolSlowMax | 调中线程池慢任务线程大小 | 100 | 最小为100 |
logretentiondays | 30 | ||
BEAT_TIMEOUT | 每X秒进行判断为不可用状态的注册节点的移除 | 30s | 可按需优化 |
DEAD_TIMEOUT | 注册节点未心跳超时时间 | 90s | 超过90s未心跳的节点将被移除,可按需优化 |
重要表结构说明:
1.XXL-JOB-GROUP 执行器
2.XXL-JOB-INFO(任务)
3.XXL-JOB-LOG(执行,调度日志)
4.XXL-JOB-REGISTRY(客户端心跳地址与时间)
5.XXL-JOB-LOCK(排他锁,集群部属时,可以防止重复执行。)
初始化说明:
public class XxlJobScheduler {
public void init() throws Exception {
// init i18n
// 国际化初始化
initI18n();
// admin registry monitor run
// 将超过90秒没有心跳的注册节点移除
// 将正在心跳的客户端注册节点维护至xxl-job-group表中的address_list字段中(可以是多个客户端用,拼接)
// 只针对执行器设置为自动注册的进行操作
// 30S执行一次。
JobRegistryMonitorHelper.getInstance().start();
// admin fail-monitor run
// 获取调度失败或者执行失败的日志
// 若配置了重试次数,将进行???
// 若配置了邮件,会发送邮件告警,可自定义
// 10s执行一次
JobFailMonitorHelper.getInstance().start();
// admin lose-monitor run
// 任务结果丢失处理
// 调度记录停留在 "运行中"(调度成功,执行状态还是默认值0) 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败;
// 60s触发一次
JobLosedMonitorHelper.getInstance().start();
// admin trigger pool start
// 调度中心
// 创建快,慢两个调度线程(主要防止慢线程阻塞,优化速度)
// 任务每分钟执行大于500ms超过10次将会标记为slow,执行时使用slow线程
JobTriggerPoolHelper.toStart();
// admin log report start
// 报表线程,处理首页的报表相关逻辑
JobLogReportHelper.getInstance().start();
// start-schedule
// 创建2个线程 :scheduleThread, ringThread,这个接口逻辑比前端的复杂一次,下面会补充详细代码
//
// scheduleThread :
// 使用xxl_job_lock表中的lockname=schedule_lock的数据进行分布式排他锁, 获取可处理数量(trigger中快慢两个线程池中的最大线程数相加 * qps 目前xxl计算出来qps=20)
// 根据当前时间,查询出来在xxl_job_info表中下次执行时间小于当前时间的所有job,数量为之前获取的可处理数量。
//
// ringThread:
// 基于秒级的任务调试中心
JobScheduleHelper.getInstance().start();
logger.info(">>>>>>>>> init xxl-job admin success.");
}
}
scheduleThread 大致执行任务逻辑
JobScheduleHelper.getInstance().start():
public void start(){
// schedule thread
scheduleThread = new Thread(new Runnable() {
@Override
public void run() {
try {
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();
Connection conn = null;
Boolean connAutoCommit = null;
PreparedStatement preparedStatement = null;
boolean preReadSuc = true;
try {
// 获取数据库连接
conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection();
// 备份一下
connAutoCommit = conn.getAutoCommit();
// 需要将autocommit设置为false,否则select执行之后自动提交事务
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();
// 查询下次执行时间最大为五秒内的任务(下次执行时间<=now+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());
// 当前时间-5s < 执行时间 < 当前时间
} else if (nowTime > jobInfo.getTriggerNextTime()) {
// 2.2、trigger-expire < 5s:direct-trigger && make next-trigger-time
// 1、trigger
// 扔给调度中心进行调度,同一个job每分钟超过500ms十次之后使用slow线程池
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
// 下次执行时间,如果在5秒之内使用ring线程继续执行
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
// 扔进ring的待执行map
// map为Map<Integer,List<Integer>> 结构 key为要执行的秒,value为job ids
// ringThread会从map中基于秒拿出需要执行的job
pushTimeRing(ringSecond, jobInfo.getId());
// 3、fresh next
//基于job的下次执行时间,在刷新一下执行时间
refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
}
} else {
// 2.3、trigger-pre-read:time-ring trigger && make next-trigger-time
// 这块跟上面差不多,这边的都是执行时间在当前时间之后的了属于未来N (N<=5) 秒之内需要执行的任务
// 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;
}
// 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
if (cost < 1000) { // scan-overtime, not wait
try {
// pre-read period: success > scan each second; fail > skip this period;
// 执行时间少于1S
// 有调度任务:1s之内随机sleep;
// 无调度任务: 5s之内随机sleep; 为什么是5s之内?因为调度任务是查的当前时间+5s的
// preReadSuc: 是否有调度任务
// PRE_READ_MS:5000
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();
// ring thread
ringThread = new Thread(new Runnable() {
@Override
public void run() {
// align second
try {
TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis()%1000 );
} catch (InterruptedException e) {
if (!ringThreadToStop) {
logger.error(e.getMessage(), e);
}
}
while (!ringThreadToStop) {
try {
// second data
List<Integer> ringItemData = new ArrayList<>();
int nowSecond = Calendar.getInstance().get(Calendar.SECOND); // 避免处理耗时太长,跨过刻度,向前校验一个刻度;
for (int i = 0; i < 2; i++) {
//private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();
// 取出当前秒-i的任务
// 循环两次,比如当前时间为59,取出58,57秒所有的任务;
// 其实为了避免处理时间太长,已经往前两个刻度校验了
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);
}
}
// next second, align second
try {
TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis()%1000);
} catch (InterruptedException e) {
if (!ringThreadToStop) {
logger.error(e.getMessage(), e);
}
}
}
logger.info(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread stop");
}
});
ringThread.setDaemon(true);
ringThread.setName("xxl-job, admin JobScheduleHelper#ringThread");
ringThread.start();
}
以上,就是本文全部内容,其实XXL中还有很多思想和有趣的实现, 希望感兴趣的读者可以更深入阅读一下源码。