xxl-job执行器的任务执行流程

363 阅读4分钟

本文结合源码,讲解了调度平台的一个调度请求,在执行器中如何被处理? image.png

一 接收run请求

当调度平台向某个执行器发送run指令时,会被EmbedServer的内部类EmbedHttpServerHandler,使用线程池异步处理。 那么本次调度就成功触发了。 run请求的参数如下

{
    "jobId": 1,
    "executorHandler": "demoJobHandler",
    "executorParams": "",
    "executorBlockStrategy": "SERIAL_EXECUTION",
    "executorTimeout": 0,
    "logId": 2,
    "logDateTime": 1728027839906,
    "glueType": "BEAN",
    "glueSource": "",
    "glueUpdatetime": 1541254891000,
    "broadcastIndex": 0,
    "broadcastTotal": 1
}

二 向JobThread提交任务

接着会调用ExecutorBizImpl#runimage.png 首先根据入参中jobId,从jobThreadRepository查找JobThread:每个任务体在执行器中,由固定的某个线程来运行。

// key是jobId
private static ConcurrentMap<Integer, JobThread> jobThreadRepository = new ConcurrentHashMap<Integer, JobThread>();

2.1 JobThread为null时

  • 某任务第一次执行时,才会向jobThreadRepository延迟注册一个JobThread。 image.png
  • 根据入参中的executorHandler,从ConcurrentMap<String, IJobHandler> jobHandlerRepository中查找任务体jobHandler(执行器启动中就初始化了)。 image.png
  • 用jobId、jobHandler创建JobThread实例并注册到jobThreadRepository中。 image.png
  • 调用JobThread.pushTriggerQueue,将本次执行请求入队,向调度平台响应调度成功。

2.2 JobThread不为null时

JobThread不为null时,会增加对入参中executorBlockStrategy的处理。

当JobThread为null时,本次执行时会新建JobThread实例,也就不可能有积压的任务;也就不用做阻塞处理。

因为本次任务只是提交到JobThread的队列中,如果队列中有积压的任务,或者JobThread正在执行某个任务,本次请求并不会被立即执行,即被阻塞

任务阻塞时,处理策略有三个: image.png image.png

使用Discard Later时,本次调度将失败。 image.png 使用Cover Early时,本次调度成功;队列中等待的任务将不会被执行,但是正在执行中的任务体,如果不能响应interrupt,还是会被完整的执行掉。 image.png

三 JobThread类

3.1 创建实例

JobThread继承了Thread类,增加了以下属性: image.png 执行器给每个任务,都会创建一个JobThread实例。 image.png 即同一执行器上的不同任务间,是线程隔离的。不会因为任务A阻塞或异常,而影响任务B的执行。

3.2 线程任务

JobThread重写了run方法,主要流程如下: image.png 注意这几点:

  • triggerQueue的尺寸是Integer.MAX_VALUE,相当于无界;

  • 为了防止一个任务的logId被并发使用,pushTriggerQueue中使用triggerLogIdSet做了校验:重复请求不入队,直接响应失败。 image.png

  • 从队列获取任务时,使用3秒超时的poll方法,因为take()在队列为空时将一直阻塞,导致不能及时检查toStop标志; image.png

  • idleTimes用于记录从队列中未获取到任务的次数,当idleTimes > 30即JobThread空闲超过30*3=90秒时,会将JobThread终结,以释放资源。

  • 只要从队列中获得任务,就将idleTimes重置为0;

  • 对于设置了超时时间的任务,会将任务体包装为FutureTask,创建一个新线程来执行,通过带超时的FutureTask.get,来检查任务是否超时。

  • 没有超时时间的任务,在JobThread中运行; image.png

注意,当一个任务的执行周期大于90秒时,每次执行,都得重新创建JobThread。

因为任务周期很长时,如1天执行一次;那一天之内,JobThread一直在空转,资源浪费严重。不如及时终止它,下次需要时再创建。

四 结果回调

4.1 结果入队

当JobThread处理完一个任务后,会将结果封装为HandleCallbackParam实例,调用TriggerCallbackThread.pushCallBack,添加到TriggerCallbackThread的callBackQueue中。 image.png image.png

4.2 结果回调

TriggerCallbackThread是在XxlJobExecutor.start()中被创建并启动。

有两个守护线程:首次回调线程、失败重试线程

// 队列
private LinkedBlockingQueue<HandleCallbackParam> callBackQueue = new LinkedBlockingQueue<HandleCallbackParam>();

// 消费队列的线程
private Thread triggerCallbackThread;
// 回调失败重试线程
private Thread triggerRetryCallbackThread;

private volatile boolean toStop = false;
  • callBackQueue.take()返回时,再调用drainTo获取队列中所有消息;
  • 调用AdminBizClient.callback,向任意一个调度平台批量推送; image.png
  • 回调失败时,将消息记录到文件中,重试线程每30秒读取一次该文件,再次推送。 image.png

五 总结

  • 任务的调度处理、执行、结果回调,是3个依次执行的独立过程,由不同的线程来运行; image.png
  • 执行器中,不同任务的执行,是线程隔离,避免互相影响;
  • JobThread实例创建后会被缓存,当空闲超过90秒,就会被回收,在任务再次执行时再创建新实例;
  • 当一个任务的执行周期,小于一次执行的平均耗时时,可能导致任务在执行器中积压;默认的阻塞策略是单机串行
  • 当阻塞策略使用Cover Early,正在执行中的旧任务,和本次新提交的任务,可能会存在并发;
  • 任务体应当正确响应interrupt
  • 执行结果通过异步批量回调。