02 xxl-job执行器底层、运行模式、阻塞处理策略

1,022 阅读10分钟

问题

1. 我们的服务和监控中心直接是如何交互的?

我们知道:定时任务运行在我们自己的服务上,而监控中心又是另外一个服务,两者直接必然要存在交互。比如:
任务执行结果成功与否要发给监控中心去统计、保存入库,这个时候我们的程序就是客户端了==实现方式==> AdminBizClient就是运行在我们的程序上,监控中心则暴露restful接口

监控中心要能控制我们的定时任务,比如基本的启动和暂停,这个时候我们的程序就是服务端了==实现方式== >我们的程序启动一个netty服务端,需要主动把我们的ip和端口告诉给监控中心,监控中心则通过ExecutorBizClient下发控制命令给客户端

2. 我们的服务和监控中心两者交互,必然要知道对方的ip和port,那ip和port是如何暴露的?

2.1 我们的服务如何知道调度中心的地址呢?

需要我们手动配置:

xxlJobSpringExecutor.setAdminAddresses("http://localhost:8080/xxl-job-admin");

2.2 调度中心如何知道我们服务的ip和port呢?

我们的程序在启动的时候通过异步线程调用AdminBizClient.registry()发送ip和port给调度中心

3. 任务的运行模式

# 04 xxl-job任务的7种运行模式(附带GLUE(Java)DEMO)

4. 任务的阻塞处理策略

# 07 xxl-job任务的3种阻塞处理策略

5. 任务的调度过期策略

# 06 xxl-job的2种调度过期策略

6. 调度中心通过快慢线程池异步通知执行器执行job

# 08 xxl-job快、慢线程池

# XxlJobSpringExecutor 执行流程图

image.png

1. afterSingletonsInstantiated() 执行器的核心入口

@Override  
public void afterSingletonsInstantiated() {  
  
    // init JobHandler Repository  
    /*initJobHandlerRepository(applicationContext);*/  

    // init JobHandler Repository (for method)  
    /*  
    * 1.扫描spring中所有bean,依次执行以下逻辑:  
    * 2.如果该bean标记了@Lazy,直接跳过  
    * 3.遍历该bean下所有标记了@XxlJob的方法,每一个方法对应一个MethodJobHandler  
    * 4.注册到XxlJobExecutor.jobHandlerRepository中  
    */  
    initJobHandlerMethodRepository(applicationContext);  

    // refresh GlueFactory  
    // 只是实例化了一个SpringGlueFactory,可以跳过  
    GlueFactory.refreshInstance(1);  

    // super start  
    try {  
        // 核心启动流程  
        super.start();  
    } catch (Exception e) {  
        throw new RuntimeException(e);  
    }  
}

1.1 扫描spring中所有bean下标记了@XxlJob的方法封装成MethodJobHandler

扫描spring中所有bean,依次执行以下逻辑:

  1. 如果该bean标记了 @Lazy ,直接跳过
  2. 遍历该bean下所有标记了 @XxlJob 的方法,每一个方法对应一个 MethodJobHandler
  3. 最终调用 registJobHandler() 注册到 XxlJobExecutor.jobHandlerRepository
// registry jobhandler  
registJobHandler(name, new MethodJobHandler(bean, executeMethod, initMethod, destroyMethod));

其中name对应@XxlJob的value(),executeMethod就是标记@XxlJob的方法,executeMethod对应@XxlJob的init(),destroyMethod对应@XxlJob的destroy()

private static ConcurrentMap<String, IJobHandler> jobHandlerRepository = new ConcurrentHashMap<String, IJobHandler>();  

public static IJobHandler registJobHandler(String name, IJobHandler jobHandler) {  
    return jobHandlerRepository.put(name, jobHandler);  
}

1.2 GlueFactory.refreshInstance(1)

只是实例化了一个SpringGlueFactory,可以跳过

public static void refreshInstance(int type){  
    if (type == 0) {  
        glueFactory = new GlueFactory();  
    } else if (type == 1) {  
        glueFactory = new SpringGlueFactory();  
    }  
}

1.3 核心启动逻辑 super.start()

start()中虽然调用了5个方法,但是其中2个跟日志有关,可以直接忽略

public void start() throws Exception {  
  
    // init logpath  
    // 初始化日志文件位置,可以忽略  
    XxlJobFileAppender.initLogPath(logPath);  

    // init invoker, admin-client  
    // adminAddresses是指当前服务要注册的xxl-job调度中心地址,多个支持逗号分隔  
    // 其实啥也没干,就是初始化adminBizList(远程调度中心)  
    initAdminBizList(adminAddresses, accessToken);  

    // init JobLogFileCleanThread  
    // 跟日志清理有关,可以忽略  
    JobLogFileCleanThread.getInstance().start(logRetentionDays);  

    // init TriggerCallbackThread  
    // 启动了2个守护线程:  
    // triggerCallbackThread  
    // triggerRetryCallbackThread  
    TriggerCallbackThread.getInstance().start();  

    // init executor-server  
    // 启动一个netty服务器接受来自调度中心的指令(比如启动、停止)  
    // 启动一个守护线程registryThread(注册):http调用api/registry发送自身的RegistryParam(包括appname以及本机ip和端口)给调度中心  
    initEmbedServer(address, ip, port, appname, accessToken);  
}

1.3.1 初始化adminBizList

adminAddresses是指当前服务要注册的xxl-job调度中心地址,多个支持逗号分隔

其实啥也没干,就是初始化adminBizList(远程调度中心)

private static List<AdminBiz> adminBizList;  
  
    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);  
            }  
        }  
    }  
}

1.3.1.1 AdminBiz

image.png

1.3.2 向调度中心发送job执行结果

TriggerCallbackThread.getInstance().start()

启动了2个守护线程:
triggerCallbackThread:不断地从 callBackQueue 队列中取出数据调用AdminBizClient.callback() 将job的执行结果发送给调度中心,如果发送失败了就将临时将job执行结果临时保存在本地的文件中。callBackQueue其实是一个LinkedBlockingQueue,其相关代码如下:

public class TriggerCallbackThread {
    LinkedBlockingQueue<HandleCallbackParam> callBackQueue
    = new LinkedBlockingQueue<>();  

    public static void pushCallBack(HandleCallbackParam callback) {  
        getInstance().callBackQueue.add(callback);  
    }
}

至于是什么时候调用pushCallBack()方法向该队列里面插入数据呢?

直接查看该方法被何处调用即可,就会发现只有在JobThread中被调用了三次

triggerRetryCallbackThread:每隔30s就将上面生成的临时文件重新解析发送给调度中心,然后删除这些临时文件

1.3.3 启动netty服务接受来自调度中心指令;暴露ip和port给调度中心

initEmbedServer(address, ip, port, appname, accessToken)

主要干了两件事:

  1. 启动一个netty服务器接受来自调度中心的指令(比如启动、停止)
  2. 启动一个守护线程registryThread(向调度中心注册当前服务):调用AdminBizClient.registry()发送自身的RegistryParam(包括appname以及本机ip和端口)给调度中心(见ExecutorRegistryThread

先看1,启动netty服务端,接受来自调度中心的控制命令,包括心跳、启动任务、停止任务等,其核心处理handler是 EmbedHttpServerHandler

image.png

EmbedHttpServerHandler的最主要的逻辑如下: image.png

我们只需要看其中的触发job和暂停job这两块逻辑就好了(由当执行器接收到调度中心的请求时,会把请求交给ExecutorBizImpl来处理),其他先直接忽略。

1.3.3.1 触发job

@Override  
public ReturnT<String> run(TriggerParam triggerParam) {  
    // load old:jobHandler + jobThread  
    JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId());  
    IJobHandler jobHandler = jobThread != null ? jobThread.getHandler() : null;  
    String removeOldReason = null;  

    // valid:jobHandler + jobThread  
    GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType());  
    if (GlueTypeEnum.BEAN == glueTypeEnum) {  
        // 当运行模式为BEAN的时候,JobHandler必填(对应@XxlJob的value),此种模式最常用
        // new jobhandler  
        IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler(triggerParam.getExecutorHandler());  

        // valid old jobThread  
        if (jobThread != null && jobHandler != newJobHandler) {  
            // change handler, need kill old thread  
            removeOldReason = "change jobhandler or glue type, and terminate the old job thread.";  

            jobThread = null;  
            jobHandler = null;  
        }  

        // valid handler  
        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) {  
        // 对应GLUE(Java)
        // valid old jobThread  
        if (jobThread != null &&  
            !(jobThread.getHandler() instanceof GlueJobHandler  
            && ((GlueJobHandler) jobThread.getHandler()).getGlueUpdatetime() == triggerParam.getGlueUpdatetime())) {  
            // change handler or gluesource updated, need kill old thread  
            removeOldReason = "change job source or glue type, and terminate the old job thread.";  

            jobThread = null;  
            jobHandler = null;  
        }  

        // valid handler  
        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()) {  

        // valid old jobThread  
        if (jobThread != null &&  
            !(jobThread.getHandler() instanceof ScriptJobHandler  
            && ((ScriptJobHandler) jobThread.getHandler()).getGlueUpdatetime() == triggerParam.getGlueUpdatetime())) {  
                // change script or gluesource updated, need kill old thread  
                removeOldReason = "change job source or glue type, and terminate the old job thread.";  

                jobThread = null;  
        jobHandler = null;  
    }  

        // valid handler  
        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.");  
    }  

    // executor block strategy  
    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  
        }  
    }  

    // replace thread (new or exists invalid)  
    if (jobThread == null) {  
        jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);  
    }  

    // push data to queue  
    ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam); 
    return pushResult;  
}

这里涉及到两个知识点:任务的运行模式和阻塞处理策略,定义和底层处理逻辑见下

1.3.4 任务的7种运行模式

image.png

决定该任务的jobHandler如何找到

当运行模式为BEAN的时候,JobHandler必填,它对应@XxlJob的value,此种模式最常用

对应的java枚举类如下:

public enum GlueTypeEnum {  
    BEAN("BEAN", false, null, null),  
    GLUE_GROOVY("GLUE(Java)", false, null, null),  
    GLUE_SHELL("GLUE(Shell)", true, "bash", ".sh"),  
    GLUE_PYTHON("GLUE(Python)", true, "python", ".py"),  
    GLUE_PHP("GLUE(PHP)", true, "php", ".php"),  
    GLUE_NODEJS("GLUE(Nodejs)", true, "node", ".js"),  
    GLUE_POWERSHELL("GLUE(PowerShell)", true, "powershell", ".ps1");
}

以运行模式为GLUE(Java)为例

新建一个运行模式为GLUE(Java)的任务后启动,然后在操作中选择 GLUE IDE

image.png

然后就可以编写我们的java程序了,右上角保存生效即可。需要注意:

  1. 它必须要实现com.xxl.job.core.handler.IJobHandler 接口,否则在任务执行的时候报错。
  2. 支持@Resource、@Autowired、@Qualifier注入我们自己的spring bean(底层原理很简单:这个java脚本在任务触发的时候会发给我们自己的服务,我们再动态生成class文件,然后再调用SpringGlueFactory.injectService()注入spring依赖,生成最终的GlueJobHandler
  3. 在引入我们自己的spring bean的时候,需要在脚本中添加对应的import

image.png

package com.xxl.job.service.handler;

import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.IJobHandler;

import com.test.things.service.impl.sys.SysUserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;

public class DemoGlueJobHandler extends IJobHandler {
  
    @Autowired
    private ApplicationContext applicationContext;
    
    @Autowired
    private SysUserServiceImpl userService;

	@Override
	public void execute() throws Exception {
		XxlJobHelper.log("XXL-JOB, Hello World.");
      
      System.out.println(applicationContext);
        System.out.println(userService.getClass().getName());
    }

}

如果你需要修改任务的逻辑,只需要重新编辑即可,不需要重启服务

1.3.5任务的3种阻塞处理策略:单机串行、丢弃后续调度、覆盖之前调度

image.png 当一个job正在运行的时候,如果发生以下情况该如何处理:

  1. 调度中心又主动触发该任务
  2. 该job耗时很长,到了其下次执行时间时,该job都还没执行完成,调度中心又发送了运行命令

一句话总结就是如果该任务发生阻塞了,该咋办

这个时候就该我们的阻塞处理策略上场了

对应的java枚举类如下:

public enum ExecutorBlockStrategyEnum {  
    SERIAL_EXECUTION("Serial execution"),  
    DISCARD_LATER("Discard Later"),  
    COVER_EARLY("Cover Early");
}

阻塞处理策略只在以下代码中被使用到

// executor block strategy  
if (jobThread != null) {  
    // 任务的阻塞处理策略  
    ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerParam.getExecutorBlockStrategy(), null);  
    if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) {  
        // discard when running 丢弃后续调度  
        if (jobThread.isRunningOrHasQueue()) {  
            return new ReturnT<>(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 串行调度  
    }  
}

详细分析三种策略的不同及底层处理

单机串行:不理会之前的任务,直接将新的任务加入到任务队列(先入先出,实现串行)中,这个队列是 LinkedBlockingQueue triggerQueue,然后由JobThread线程不断轮询该队列,取出我们的job去执行我们的业务代码;

image.png

丢弃后续调度:如果该任务对应的JobThread正在处理任务或有任务要处理,就直接不管新的任务了,也不理会之前的任务,流程走完;否则同单机串行

if (jobThread.isRunningOrHasQueue()) { // 该任务对应的JobThread正在处理任务或有任务要处理  
    // 直接流程走完,也不理会之前的任务  
    return new ReturnT<>(ReturnT.FAIL_CODE, "block strategy effect:" + ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle());  
}

覆盖之前调度:如果该任务对应的JobThread正在处理任务或有任务要处理,就将之前的任务丢掉并中断处理(下面江如何中断),创建一个新的任务;否则同单机串行

if (jobThread.isRunningOrHasQueue()) {  // 该任务对应的JobThread正在处理任务或有任务要处理
    removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle();  
    // 丢掉之前的任务,等会重新创建一个  
    jobThread = null;  
}

在上面的代码中,如果jobThread有任务要处理,我们就将jobThread设置为null,然后重新创建一个新的任务。那么中断旧的任务?代码见下:

// replace thread (new or exists invalid)  
if (jobThread == null) {  
    jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);  
}

public static JobThread registJobThread(int jobId, IJobHandler handler, String removeOldReason) {  
    JobThread newJobThread = new JobThread(jobId, handler);  
    newJobThread.start();  

    JobThread oldJobThread = jobThreadRepository.put(jobId, newJobThread); // putIfAbsent | oh my god, map's put method return the old value!!!  
    if (oldJobThread != null) {  
        oldJobThread.toStop(removeOldReason);  
        oldJobThread.interrupt();  
    } 
    return newJobThread;  
}

中断旧任务的核心代码就两行:

oldJobThread.toStop(removeOldReason);
// 中断线程后,JobThread抛出中断异常,我们捕获该异常后再自己处理业务 oldJobThread.interrupt();

中断是通过Thread#interrupt方法实现的,所以正在处理的任务还是有可能继续运行,并不是说一中断正在运行的任务就终止了

1.3.3.2 停止job

@Override  
public ReturnT<String> kill(KillParam killParam) {  
    // kill handlerThread, and create new one  
    JobThread jobThread = XxlJobExecutor.loadJobThread(killParam.getJobId());  
    if (jobThread != null) {  
        XxlJobExecutor.removeJobThread(killParam.getJobId(), "scheduling center kill job.");  
        return ReturnT.SUCCESS;  
    }
    return new ReturnT<String>(ReturnT.SUCCESS_CODE, "job thread already killed.");  
}

1.3.3.3 JobThread