问题
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. 任务的阻塞处理策略
5. 任务的调度过期策略
6. 调度中心通过快慢线程池异步通知执行器执行job
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,依次执行以下逻辑:
- 如果该bean标记了 @Lazy ,直接跳过
- 遍历该bean下所有标记了 @XxlJob 的方法,每一个方法对应一个 MethodJobHandler
- 最终调用 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
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)
主要干了两件事:
- 启动一个netty服务器接受来自调度中心的指令(比如启动、停止)
- 启动一个守护线程registryThread(向调度中心注册当前服务):调用AdminBizClient.registry()发送自身的RegistryParam(包括appname以及本机ip和端口)给调度中心(见ExecutorRegistryThread)
先看1,启动netty服务端,接受来自调度中心的控制命令,包括心跳、启动任务、停止任务等,其核心处理handler是 EmbedHttpServerHandler
EmbedHttpServerHandler的最主要的逻辑如下:
我们只需要看其中的触发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种运行模式
决定该任务的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
然后就可以编写我们的java程序了,右上角保存生效即可。需要注意:
- 它必须要实现com.xxl.job.core.handler.IJobHandler 接口,否则在任务执行的时候报错。
- 支持@Resource、@Autowired、@Qualifier注入我们自己的spring bean(底层原理很简单:这个java脚本在任务触发的时候会发给我们自己的服务,我们再动态生成class文件,然后再调用SpringGlueFactory.injectService()注入spring依赖,生成最终的GlueJobHandler)
- 在引入我们自己的spring bean的时候,需要在脚本中添加对应的import
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种阻塞处理策略:单机串行、丢弃后续调度、覆盖之前调度
当一个job正在运行的时候,如果发生以下情况该如何处理:
- 调度中心又主动触发该任务
- 该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去执行我们的业务代码;
丢弃后续调度:如果该任务对应的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.");
}