【xxl-job源码篇01】xxl-job源码解读 神奇的时间轮 触发流程解读

导读

xxl-job是一个分布式任务调度平台,在业内深受广大程序员的喜爱,本章将带你深入了解xxl-job的源码,理解其运行逻辑。

阅读xxl-job源码会增强你对多线程的理解与应用,调度思想的升华

本章将带你一点点剖析xxl-job设计底层逻辑,让你真正理解下图的每一个模块,让你知其然更知其所以然。

输入图片说明

项目结构

拉下代码我们首先看项目结构,xxl-job为标准的父子工程,我们看到有三个子项目

image-20220412144123632

  • xxl-job-admin 服务端
  • xxl-job-core 核心包,会被服务端和客户端同时引用
  • xxl-job-executor-samples 一些demo

源码解读——定时器

ok,现在我们需要知道xxl-job服务端启动都干了什么,首先定位到这个类XxlJobAdminConfig

可以看到xxl-job核心功能是跟随bean的生命周期运行的

@Component
public class XxlJobAdminConfig implements InitializingBean, DisposableBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        adminConfig = this;
        xxlJobScheduler = new XxlJobScheduler();
      	// 初始化
        xxlJobScheduler.init();
    }
    @Override
    public void destroy() throws Exception {
        xxlJobScheduler.destroy();
    }
}

跟一下代码com.xxl.job.admin.core.scheduler.XxlJobScheduler#init

可以看到,此处初始化了几个核心的helper,每个helper都运用了饿汉单例模式,不会出现重复创建的情况

public void init() throws Exception {
    // 触发池
    JobTriggerPoolHelper.toStart();
    // 服务监听器
    JobRegistryHelper.getInstance().start();
    // 失败告警
    JobFailMonitorHelper.getInstance().start();
    // 回调监听器
    JobCompleteHelper.getInstance().start();
    // 日志回调
    JobLogReportHelper.getInstance().start();
    // 定时器
    JobScheduleHelper.getInstance().start();
}

首先定位到com.xxl.job.admin.core.thread.JobScheduleHelper#start

该helper就是定时器的心脏,其作用为定时和预触发。

启动了两个守护线程

  1. scheduleThread 任务扫描线程

    while循环,不停的扫描即将执行的任务,使用mysql for update实现排他锁,防止其他服务并行扫描,并将即将执行的任务缓存到时间轮

  2. ringThread 执行线程

    触发时间轮中的任务

时间轮

来看看时间轮这个听起来高大上的东西是如何实现的

如下,本质就是一个concurrentHashMap,key为执行的秒,value为要执行的job的id,scheduleThread线程会提前5-10秒将任务放入时间轮的list中。

private volatile static Map<Integer, List<Integer>> ringData = new ConcurrentHashMap<>();
// ringSecond 下一次执行的时间
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);                              
}

触发时间轮中的任务

ringThread = new Thread(new Runnable() {
    @Override
    public void run() {
        while (!ringThreadToStop) {
            try {
              // 时间对其
                TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);
            } catch (InterruptedException e) {
                if (!ringThreadToStop) {
                    logger.error(e.getMessage(), e);
                }
            }
            try {
                // 时间轮数据处理
                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);
                    }
                }
                if (ringItemData.size() > 0) {
                    // 触发任务
                    for (int jobId: ringItemData) {
                        // 触发任务
                        JobTriggerPoolHelper.trigger(jobId, TriggerTypeEnum.CRON, -1, null, null, null);
                    }
                    ringItemData.clear();
                }
            } catch (Exception e) {
                if (!ringThreadToStop) {
                    logger.error(">>>>>>>>>>> xxl-job, JobScheduleHelper#ringThread error:{}", e);
                }
            }
        }
    }
});

未命名文件

如上图,时间轮的总长度是60,对应的是一分中的每一秒,前面说了,scheduleThread会提前5-10秒将任务放入时间轮中,ringThread会将要执行的任务从时间轮中移除,也就是说存取不存在并发操作。由于定时任务的特殊业务,时间轮的并发也非常低。

面试题

为什么要用两个线程

将扫描线程和执行线程隔离,因为扫描线程需要和数据库交互,且使用了排他锁,性能较慢。

执行线程不与中间件交互,直接扫描时间轮,性能较高,主要用于保证任务精准触发。

时间轮有什么好处

逻辑无锁,性能高效,两个线程没有执行上的冲突。

如何保证在精确的时间执行

通过如下代码将当前时间与整秒对其,可精确到毫秒

TimeUnit.MILLISECONDS.sleep(1000 - System.currentTimeMillis() % 1000);

源码解读——触发

本段会跟踪xxl-job任务从定时器->触发器->http request->触发任务

com.xxl.job.admin.core.thread.JobTriggerPoolHelper#start

触发器会初始化两个用于触发任务的线程池

com.xxl.job.admin.core.thread.JobTriggerPoolHelper#addTrigger调用触发器时不会直接调度,而是会使用线程池去执行,其目的在于不阻塞调度线程

public void addTrigger(final int jobId,
                       final TriggerTypeEnum triggerType,
                       final int failRetryCount,
                       final String executorShardingParam,
                       final String executorParam,
                       final String addressList) {

    // 默认使用fastTriggerPool
    ThreadPoolExecutor triggerPool_ = fastTriggerPool;
    AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
    // 如果发现任务一分钟内有大于10次的慢执行,换slowTriggerPool线程池
    if (jobTimeoutCount!=null && jobTimeoutCount.get() > 10) {
        triggerPool_ = slowTriggerPool;
    }

    // 线程池执行
    triggerPool_.execute(new Runnable() {
        @Override
        public void run() {
            long start = System.currentTimeMillis();
            try {
                // 触发
                XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            } finally {
                // 到达下一个周期则清理上一个周期数据
                long minTim_now = System.currentTimeMillis()/60000;
                if (minTim != minTim_now) {
                    minTim = minTim_now;
                    jobTimeoutCountMap.clear();
                }

                // 记录慢任务执行次数
                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();
                    }
                }

            }

        }
    });
}

顺着以下链路

com.xxl.job.admin.core.trigger.XxlJobTrigger#processTrigger com.xxl.job.admin.core.trigger.XxlJobTrigger#runExecutor

以下就是触发任务的方法

runResult = executorBiz.run(triggerParam);

此处有两个实现 ,xxl-job端用的是ExecutorBizClient,我们自己的应用生效的是ExecutorBizImpl

image-20220412171159090

接下来看一下ExecutorBizImpl的实现

注意:此时server端通过ExecutorBizClient已经调度到了我们自己的项目中了

@Override
public ReturnT<String> run(TriggerParam triggerParam) {
    // 获得执行控制器
    JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId());
    IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null;
    String removeOldReason = null;

    GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType());
    if (GlueTypeEnum.BEAN == glueTypeEnum) {    // bean模式触发
        IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler(triggerParam.getExecutorHandler());
        if (jobThread!=null && jobHandler != newJobHandler) {
            removeOldReason = "change jobhandler or glue type, and terminate the old job thread.";
            jobThread = null;
            jobHandler = null;
        }
        if (jobHandler == null) {
            jobHandler = newJobHandler;
            if (jobHandler == null) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "job handler [" + triggerParam.getExecutorHandler() + "] not found.");
            }
        }
    } else if (GlueTypeEnum.GLUE_GROOVY == glueTypeEnum) {  // 原生模式触发
        if (jobThread != null &&
                !(jobThread.getHandler() instanceof GlueJobHandler
                    && ((GlueJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
            removeOldReason = "change job source or glue type, and terminate the old job thread.";

            jobThread = null;
            jobHandler = null;
        }
        if (jobHandler == null) {
            try {
                IJobHandler originJobHandler = GlueFactory.getInstance().loadNewInstance(triggerParam.getGlueSource());
                jobHandler = new GlueJobHandler(originJobHandler, triggerParam.getGlueUpdatetime());
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                return new ReturnT<String>(ReturnT.FAIL_CODE, e.getMessage());
            }
        }
    } else if (glueTypeEnum!=null && glueTypeEnum.isScript()) { // 脚本触发
        if (jobThread != null &&
                !(jobThread.getHandler() instanceof ScriptJobHandler
                        && ((ScriptJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
            removeOldReason = "change job source or glue type, and terminate the old job thread.";
            jobThread = null;
            jobHandler = null;
        }
        if (jobHandler == null) {
            jobHandler = new ScriptJobHandler(triggerParam.getJobId(), triggerParam.getGlueUpdatetime(), triggerParam.getGlueSource(), GlueTypeEnum.match(triggerParam.getGlueType()));
        }
    } else {
        return new ReturnT<String>(ReturnT.FAIL_CODE, "glueType[" + triggerParam.getGlueType() + "] is not valid.");
    }
    // 阻塞处理策略
    if (jobThread != null) {
        ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerParam.getExecutorBlockStrategy(), null);
        if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) {
            // discard when running
            if (jobThread.isRunningOrHasQueue()) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "block strategy effect:"+ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle());
            }
        } else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) {
            // kill running jobThread
            if (jobThread.isRunningOrHasQueue()) {
                removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle();

                jobThread = null;
            }
        } else {
            // just queue trigger
        }
    }

    if (jobThread == null) {
        // 创建执行控制器
        jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
    }

    // 将数据放入执行队列
    ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);
    return pushResult;
}

注意,此处并没有等待任务执行完成,而是放入队列后直接返回触发结果,执行结果会后置通知server端

进入com.xxl.job.core.thread.JobThread#pushTriggerQueue

public ReturnT<String> pushTriggerQueue(TriggerParam triggerParam) {
   // 防止重复触发的set
   if (triggerLogIdSet.contains(triggerParam.getLogId())) {
      logger.info(">>>>>>>>>>> repeate trigger job, logId:{}", triggerParam.getLogId());
      return new ReturnT<String>(ReturnT.FAIL_CODE, "repeate trigger job, logId:" + triggerParam.getLogId());
   }
   triggerLogIdSet.add(triggerParam.getLogId());
   // 加入待执行队列
   triggerQueue.add(triggerParam);
       return ReturnT.SUCCESS;
}

该类为执行handler的控制器,它继承了Thread,我们看一下run方法

com.xxl.job.core.thread.JobThread#run

   @Override
public void run() {
       // init
       try {
      handler.init();
   } catch (Throwable e) {
          logger.error(e.getMessage(), e);
   }
   while(!toStop){
      running = false;
      idleTimes++; // 增加空闲的次数

           TriggerParam triggerParam = null;
           try {
         // 将队列中待执行待任务poll出来
         triggerParam = triggerQueue.poll(3L, TimeUnit.SECONDS);
         if (triggerParam!=null) {
            running = true;
            idleTimes = 0;
            triggerLogIdSet.remove(triggerParam.getLogId());

            // 记录上下文对象,用于数据分片,日志记录等动作
            String logFileName = XxlJobFileAppender.makeLogFileName(new Date(triggerParam.getLogDateTime()), triggerParam.getLogId());
            XxlJobContext xxlJobContext = new XxlJobContext(
                  triggerParam.getJobId(),
                  triggerParam.getExecutorParams(),
                  logFileName,
                  triggerParam.getBroadcastIndex(),
                  triggerParam.getBroadcastTotal());
            XxlJobContext.setXxlJobContext(xxlJobContext);

            XxlJobHelper.log("<br>----------- xxl-job job execute start -----------<br>----------- Param:" + xxlJobContext.getJobParam());
            if (triggerParam.getExecutorTimeout() > 0) {
               // 有设置执行时间的话通过FutureTask实现等待超时的动作
               Thread futureThread = null;
               try {
                  FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new Callable<Boolean>() {
                     @Override
                     public Boolean call() throws Exception {
                        // 记录上下文对象,用于数据分片,日志记录等动作
                        XxlJobContext.setXxlJobContext(xxlJobContext);
                        handler.execute();
                        return true;
                     }
                  });
                  // 创建并执行任务线程
                  futureThread = new Thread(futureTask);
                  futureThread.start();

                  Boolean tempResult = futureTask.get(triggerParam.getExecutorTimeout(), TimeUnit.SECONDS);
               } catch (TimeoutException e) {
                  // 执行超时处理
                  XxlJobHelper.log("<br>----------- xxl-job job execute timeout");
                  XxlJobHelper.log(e);
                  XxlJobHelper.handleTimeout("job execute timeout ");
               } finally {
                  futureThread.interrupt();
               }
            } else {
               // 如果有设置执行超时时间直接执行
               handler.execute();
            }

            // 校验执行状态
            if (XxlJobContext.getXxlJobContext().getHandleCode() <= 0) {
               XxlJobHelper.handleFail("job handle result lost.");
            } else {
               // 截取日志长度,防止过长影响性能
               String tempHandleMsg = XxlJobContext.getXxlJobContext().getHandleMsg();
               tempHandleMsg = (tempHandleMsg!=null&&tempHandleMsg.length()>50000)
                     ?tempHandleMsg.substring(0, 50000).concat("...")
                     :tempHandleMsg;
               XxlJobContext.getXxlJobContext().setHandleMsg(tempHandleMsg);
            }
            XxlJobHelper.log("<br>----------- xxl-job job execute end(finish) -----------<br>----------- Result: handleCode="
                  + XxlJobContext.getXxlJobContext().getHandleCode()
                  + ", handleMsg = "
                  + XxlJobContext.getXxlJobContext().getHandleMsg()
            );
         } else {
            if (idleTimes > 30) {
               if(triggerQueue.size() == 0) {
                  // 当空闲次数大于30次且队列中无待执行时移除控制器,释放资源
                  XxlJobExecutor.removeJobThread(jobId, "excutor idel times over limit.");
               }
            }
         }
      } catch (Throwable e) {
         // 异常,记录错误日志
         if (toStop) {
            XxlJobHelper.log("<br>----------- JobThread toStop, stopReason:" + stopReason);
         }

         // handle result
         StringWriter stringWriter = new StringWriter();
         e.printStackTrace(new PrintWriter(stringWriter));
         String errorMsg = stringWriter.toString();

         XxlJobHelper.handleFail(errorMsg);

         XxlJobHelper.log("<br>----------- JobThread Exception:" + errorMsg + "<br>----------- xxl-job job execute end(error) -----------");
      } finally {
         // 将执行结果和日志通知给xxl-job
               if(triggerParam != null) {
                   if (!toStop) {
                       TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
                              triggerParam.getLogId(),
                     triggerParam.getLogDateTime(),
                     XxlJobContext.getXxlJobContext().getHandleCode(),
                     XxlJobContext.getXxlJobContext().getHandleMsg() )
               );
                   } else {
                       TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
                              triggerParam.getLogId(),
                     triggerParam.getLogDateTime(),
                     XxlJobContext.HANDLE_CODE_FAIL,
                     stopReason + " [job running, killed]" )
               );
                   }
               }
           }
       }

   // 将队列中的任务回调标记失败处理
   while(triggerQueue !=null && triggerQueue.size()>0){
      TriggerParam triggerParam = triggerQueue.poll();
      if (triggerParam!=null) {
         TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
               triggerParam.getLogId(),
               triggerParam.getLogDateTime(),
               XxlJobContext.HANDLE_CODE_FAIL,
               stopReason + " [job not executed, in the job queue, killed.]")
         );
      }
   }
   try {
      handler.destroy();
   } catch (Throwable e) {
      logger.error(e.getMessage(), e);
   }

   logger.info(">>>>>>>>>>> xxl-job JobThread stoped, hashCode:{}", Thread.currentThread());
}

注意这段

XxlJobContext.setXxlJobContext(xxlJobContext);

使用threadlocal维护了xxl-job的上下文对象,这个大有用处,在任务的执行过程中xxl-job的日志系统会用到里面维护的部分数据

通过以上代码可以看到xxl-job触发我们应用不存在阻塞等待执行完成的动作,调度成功后立马就返回,执行结果和执行日志都是后置请求通知给xxl-job的,这样做的好处是xxl-job可以快速知道服务端有没有调度成功,也不会因为任务没有执行完成而占用系统连接数。xxl-job执行线程闲置时会自动释放资源,不会持续占用系统资源。

链接

【xxl-job源码篇02】注册中心 自研RPC netty的应用?

【xxl-job源码篇03】xxl-job日志系统源码解读

  • 5
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值