一、启动配置类
Xxl-job的服务端是以springboot构架为基础打包运行的,启动类为XxlJobAdminApplication,那么就先从他的配置类XxlJobAdminConfig看起。
@Component public class XxlJobAdminConfig implements InitializingBean, DisposableBean { private static XxlJobAdminConfig adminConfig = null; public static XxlJobAdminConfig getAdminConfig() { return adminConfig; } // 调度器实例 private XxlJobScheduler xxlJobScheduler; @Override public void afterPropertiesSet() throws Exception { adminConfig = this; // 实例化定时器 xxlJobScheduler = new XxlJobScheduler(); //初始化 xxlJobScheduler.init(); } // 销毁方法 @Override public void destroy() throws Exception { xxlJobScheduler.destroy(); } ··· }
InitializingBean、DisposableBean前面已经解释过了,我们直接看XxlJobScheduler的init方法
public void init() throws Exception { // 1、国际化 initI18n(); // 2、监控客户端注册 JobRegistryMonitorHelper.getInstance().start(); // 3、执行失败的job监控 JobFailMonitorHelper.getInstance().start(); // 4、初始化了两个线程池,一快一慢 JobTriggerPoolHelper.toStart(); // 5、执行结果报表统计 定时清理xxl_job_log表的数据 JobLogReportHelper.getInstance().start(); // 6、启动调度线程 JobScheduleHelper.getInstance().start(); logger.info(">>>>>>>>> init xxl-job admin success."); }
1、国际化主要是通过I18nUtil加载国际化资源文件,这块实现充分利用spring的 EncodedResource和PropertiesLoaderUtils.loadProperties
2、监控客户端执行器的注册
之前看客户端源码的时候也讲过了,每过30秒客户端执行器会发送一次注册请求,服务端接收到请求也会更新xxl_job_registry表。这里会通过查表的方式判断执行器是否存活,针对不存活的执行器,会进行剔除操作,也是每30秒执行一次。
3、执行失败的job会根据配置来触发告警、邮件等
4、初始化两个线程池,一个快速线程池,一个慢速线程池,用了执行后续的调度任务
5、报表统计,统计了例如:成功数,失败数;另外还会清理执行日志表的数据。
6、启动调度线程,关键部分
public void start(){ // schedule thread scheduleThread = new Thread(new Runnable() { @Override public void run() { try { // 随机睡眠 4 ~ 5 秒 TimeUnit.MILLISECONDS.sleep(5000 - System.currentTimeMillis()%1000 ); } catch (InterruptedException e) { if (!scheduleThreadToStop) { logger.error(e.getMessage(), e); } } // pre-read count: treadpool-size * trigger-qps (each trigger cost 50ms, qps = 1000/50 = 20) // 这里做了一个预估 每个任务的执行时间为 50ms int preReadCount = (XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax() + XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax()) * 20; while (!scheduleThreadToStop) { 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(); conn.setAutoCommit(false); // 获取分布式锁 preparedStatement = conn.prepareStatement( "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" ); preparedStatement.execute(); long nowTime = System.currentTimeMillis(); // 查询的是 下次调度时间trigger_next_time 小于 当前时间 + 5000 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) { if (nowTime > jobInfo.getTriggerNextTime() + PRE_READ_MS) { // 当前时间 超出了 下次调度时间 5秒以上 忽略本次调度 计算下次调度时间 refreshNextValidTime(jobInfo, new Date()); } else if (nowTime > jobInfo.getTriggerNextTime()) { // 当前时间 超出 下次调度时间 5秒以内 // 放入调度线程 等待执行 JobTriggerPoolHelper.trigger(jobInfo.getId(), TriggerTypeEnum.CRON, -1, null, null); // 计算下次调度时间 refreshNextValidTime(jobInfo, new Date()); if (jobInfo.getTriggerStatus()==1 && nowTime + PRE_READ_MS > jobInfo.getTriggerNextTime()) { // 下次的执行时间在 5s 以内 int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60); // 放入 ringData pushTimeRing(ringSecond, jobInfo.getId()); // 修改下次的执行时间 refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); } } else { // 当前时间 小于 下次调度时间 5s 以内(因为查询时的条件时 下次调度时间 <= 当前时间 + 5) // 计算秒数 int ringSecond = (int)((jobInfo.getTriggerNextTime()/1000)%60); // 放入ringData 根据5秒内即将执行的任务的执行时间的秒数,将其放到timeheel对应秒数的list中 pushTimeRing(ringSecond, jobInfo.getId()); // 修改下次执行时间 refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime())); } } for (XxlJobInfo jobInfo: scheduleList) { // 修改xxl_job_info XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().scheduleUpdate(jobInfo); } } else { preReadSuc = false; } } catch (Exception e) { if (!scheduleThreadToStop) { logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#scheduleThread error:{}", e); } } finally { // 释放分布式锁 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; // 处理时间 if (cost < 1000) { // scan-overtime, not wait try { // 每次有任务成功加入到队列 睡眠 1s 否则 5s 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(); // 执行ringData里面的任务 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++) { 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); } // 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(); }
最终会调用JobTriggerPoolHelper.addTrigger(···)方法,将调度任务放到之前创建的线程池中执行
public void addTrigger(final int jobId, final TriggerTypeEnum triggerType, final int failRetryCount, final String executorShardingParam, final String executorParam) { // choose thread pool ThreadPoolExecutor triggerPool_ = fastTriggerPool; AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId); // 默认情况下,会使用fastTriggerPool。 // 如果1分钟窗口期内任务耗时达500ms超过10次,则该窗口期内判定为慢任务,慢任务自动降级进入 Slow 线程池,避免耗尽调度线程,提高系统稳定性 if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) { // job-timeout 10 times in 1 min triggerPool_ = slowTriggerPool; } // trigger triggerPool_.execute(new Runnable() { @Override public void run() { long start = System.currentTimeMillis(); try {// 执行调度 XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam); } catch (Exception e) { logger.error(e.getMessage(), e); } finally { // check timeout-count-map long minTim_now = System.currentTimeMillis()/60000; if (minTim != minTim_now) { minTim = minTim_now; jobTimeoutCountMap.clear(); } // incr timeout-count-map long cost = System.currentTimeMillis()-start; if (cost > 500) { // ob-timeout threshold 500ms AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1)); if (timeoutCount != null) { timeoutCount.incrementAndGet(); } } } } }); }
执行调度
public static void trigger(int jobId, TriggerTypeEnum triggerType, int failRetryCount, String executorShardingParam, String executorParam) { // load data XxlJobInfo jobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(jobId); if (jobInfo == null) { logger.warn(">>>>>>>>>>>> trigger fail, jobId invalid,jobId={}", jobId); return; } if (executorParam != null) { jobInfo.setExecutorParam(executorParam); } int finalFailRetryCount = failRetryCount>=0?failRetryCount:jobInfo.getExecutorFailRetryCount(); // 分片 XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(jobInfo.getJobGroup()); // sharding param // 组装分片参数 int[] shardingParam = null; if (executorShardingParam!=null){ String[] shardingArr = executorShardingParam.split("/"); if (shardingArr.length==2 && isNumeric(shardingArr[0]) && isNumeric(shardingArr[1])) { shardingParam = new int[2]; shardingParam[0] = Integer.valueOf(shardingArr[0]); shardingParam[1] = Integer.valueOf(shardingArr[1]); } } // 路由策略 if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) && group.getRegistryList()!=null && !group.getRegistryList().isEmpty() && shardingParam==null) { // 分片广播 for (int i = 0; i < group.getRegistryList().size(); i++) { processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size()); } } else { if (shardingParam == null) { shardingParam = new int[]{0, 1}; } processTrigger(group, jobInfo, finalFailRetryCount, triggerType, shardingParam[0], shardingParam[1]); } }
通过processTrigger方法,最终调到XxlJobTrigger的runExecutor方法
public static ReturnT<String> runExecutor(TriggerParam triggerParam, String address){ ReturnT<String> runResult = null; try { // 创建NettyHttpClient客户端 // 通过XxlRpcReferenceBean.getObject()获取ExecutorBiz的代理对象 jdk动态代理 ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address); // 调用代理对象的run方法,也就是InvocationHandler的invoke方法 // 通过NettyHttpClient发送http请求,客户端收到请求就会执行对应的调度方法 runResult = executorBiz.run(triggerParam); } catch (Exception e) { logger.error(">>>>>>>>>>> xxl-job trigger error, please check if the executor[{}] is running.", address, e); runResult = new ReturnT<String>(ReturnT.FAIL_CODE, ThrowableUtil.toString(e)); } StringBuffer runResultSB = new StringBuffer(I18nUtil.getString("jobconf_trigger_run") + ":"); runResultSB.append("<br>address:").append(address); runResultSB.append("<br>code:").append(runResult.getCode()); runResultSB.append("<br>msg:").append(runResult.getMsg()); runResult.setMsg(runResultSB.toString()); return runResult; }
代码还是非常简单的,这里就只贴出一部分就好了,总结一下:
1、调度中心 向 执行器 发送http调度请求,执行器接收请求的服务,实际上是一台内嵌Server,默认端口9999
2、执行器执行任务逻辑,这里是异步执行
3、执行器异步使用http回调调度中心反馈调度结果,调度中心中接收回调的服务,是针对执行器开放一套API服务
二、架构图
剩下还有一些Controller对应了调用中心的页面操作,这里就不分析了,代码写的还是很不错的,有参考意义。