XXL-JOB--执行器篇
Github地址: github.com/xuxueli/xxl…
一 引言
说到定时任务,大家一定不陌生,我们做项目必然会有大量需求需要使用定时任务,定时清理脏数据,定时清理备份数据等等
下面罗列了一下定时任务的一些基础实现方案
1 Thread
最简单的定时任务,实现起来也十分简单
while(true){
doSomething();
TimeUnit.DAYS.sleep(1);
}
2 Timer、ScheduledExecutorService
TimerTask、ScheduledExecutorService分别是JDK1.3 JDK1.5提供的定时任务框架
3 spring task
spring task 的核心注解是 @Scheduled,原理其实非常简单,就是帮我们创建自动ScheduledExecutorService并注入任务,有兴趣可以去看一下spring提供的bean后置处理器ScheduledAnnotationBeanPostProcessor。
但是这些定时任务都有一个最大的缺点,在如今动不动就是分布式架构的大环境下,有时候我们只想一个微服务的实例去执行任务,这些居于JDK 主动执行 的定时任务框架,想要指定机器执行就变得力不从心了。
下面引出本文主角XXL-JOB
二 XXL-JOB 简介
官网文档: www.xuxueli.com/xxl-job/
这里就不简单介绍了,想要了解及使用可以阅读官方文档,这是官网给出的架构图
XXL-JOB主要分为两部分:调度中心、执行器,回到上面的问题,如何保证只有一台机器执行定时任务。
执行这块 xxl-job给出的方案是由外部被动触发,通过调度中心调用执行器接口来执行。而想要做到指定机器,这点和注册中心一样,只需要维护一个服务地址的集合,需要执行任务时调用指定机器的接口即可。
三 执行器 xxl-job-core
如果是spring服务,xxljob会帮我们完成所有功能类的创建,和定时任务的自动注入,一切都要归功于XxlJobSpringExecutor,它继承了SmartInitializingSingleton,在所有单例bean完成初始化时,会回调afterSingletonsInstantiated()方法。
public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, SmartInitializingSingleton, DisposableBean {
@Override
public void afterSingletonsInstantiated() {
// 扫描所有带有@XxlJob注解的方法 创建IJobHandler对象
// 并注册到以name为key,IJobHandler为value的map中,方便获取
initJobHandlerMethodRepository(applicationContext);
// 获取GlueFactory实例工厂,不常用
// 主要功能是吧调度中心的java代码转换成IJobHandler类对象
GlueFactory.refreshInstance(1);
// super start
try {
super.start();
} catch (Exception e) {throw new RuntimeException(e);}
}
}
3.1 initJobHandlerMethodRepository
private void initJobHandlerMethodRepository(ApplicationContext applicationContext) {
if (applicationContext == null) {
return;
}
// 拿到所有单例bean的名称
String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class, false, true);
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = applicationContext.getBean(beanDefinitionName);
// 拿到所有bean对象,遍历所有方法解析带有@xxljob的方法
Map<Method, XxlJob> annotatedMethods = null;
try {
annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
new MethodIntrospector.MetadataLookup<XxlJob>() {
@Override
public XxlJob inspect(Method method) {
return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
}
});
} catch (Throwable ex) {
logger.error("xxl-job method-jobhandler resolve error for bean[" + beanDefinitionName + "].", ex);
}
if (annotatedMethods==null || annotatedMethods.isEmpty()) {
continue;
}
// 遍历获得的Map<Method, XxlJob>对象
for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
Method method = methodXxlJobEntry.getKey();
XxlJob xxlJob = methodXxlJobEntry.getValue();
// 校验注解及注解的值的正确性
if (xxlJob == null) {continue;}
String name = xxlJob.value();
if (name.trim().length() == 0) {
throw new RuntimeException("xxl-job method-jobhandler name invalid, for[" + bean.getClass() + "#" + method.getName() + "] .");}
if (loadJobHandler(name) != null) {
throw new RuntimeException("xxl-job jobhandler[" + name + "] naming conflicts.");}
// 强制指定方法参数是String 类型,返回值是ReturnT<String>
if (!(method.getParameterTypes().length == 1 && method.getParameterTypes()[0].isAssignableFrom(String.class))) {
throw new RuntimeException("xxl-job method-jobhandler param-classtype invalid, for[" + bean.getClass() + "#" + method.getName() + "] , " +
"The correct method format like " public ReturnT<String> execute(String param) " .");}
if (!method.getReturnType().isAssignableFrom(ReturnT.class)) {
throw new RuntimeException("xxl-job method-jobhandler return-classtype invalid, for[" + bean.getClass() + "#" + method.getName() + "] , " +
"The correct method format like " public ReturnT<String> execute(String param) " .");}
// 指定方法是可访问的,防止反射调用失败
method.setAccessible(true);
// 初始化 注解上的两个方法
Method initMethod = null;
Method destroyMethod = null;
// 校验注解上的方法并设置访问权限
if (xxlJob.init().trim().length() > 0) {
try {
initMethod = bean.getClass().getDeclaredMethod(xxlJob.init());
initMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException("xxl-job method-jobhandler initMethod invalid, for[" + bean.getClass() + "#" + method.getName() + "] .");
}
}
if (xxlJob.destroy().trim().length() > 0) {
try {
destroyMethod = bean.getClass().getDeclaredMethod(xxlJob.destroy());
destroyMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new RuntimeException("xxl-job method-jobhandler destroyMethod invalid, for[" + bean.getClass() + "#" + method.getName() + "] .");
}
}
// 注册JobHandler
registJobHandler(name, new MethodJobHandler(bean, method, initMethod, destroyMethod));
}
}
}
// 这是一个线程安全的map对象
private static ConcurrentMap<String, IJobHandler> jobHandlerRepository = new ConcurrentHashMap<String, IJobHandler>();
public static IJobHandler registJobHandler(String name, IJobHandler jobHandler){
logger.info(">>>>>>>>>>> xxl-job register jobhandler success, name:{}, jobHandler:{}", name, jobHandler);
return jobHandlerRepository.put(name, jobHandler);
}
和以往定时任务不同的是IJobHandler对象非常简单,以MethodJobHandler为例,只维护了反射的两个必要参数(方法参数执行是提供,且限定了String类型),和初始、销毁时的调用方法。
public class MethodJobHandler extends IJobHandler {
private final Object target;
private final Method method;
private Method initMethod;
private Method destroyMethod;
public MethodJobHandler(Object target, Method method, Method initMethod, Method destroyMethod) {
this.target = target;
this.method = method;
this.initMethod =initMethod;
this.destroyMethod =destroyMethod;
}
。。。。。
}
有关于方法应该什么时候执行,多久执行一次,执行参数是什么我们都不用关心,全盘交由调度中心,我们只需要负责好我们的业务逻辑即可。
到这里,我们已经准备好了我们的jobHandlerRepository(ConcurrentMap<String, IJobHandler>,map的key是我们@Xxljob注解的name属性,map的value是执行反射的必要信息。那接下来如果要完成基础功能,调度中心只需要告诉执行器相应的IJobHandler名称name,我们就能具体准确的完成定时任务的调用。
3.2 start启动
接下来是XxlJobExecutor的启动(即调用父类的start()方法),可以分为五步。
public void start() throws Exception {
// 1 初始化日志路径
XxlJobFileAppender.initLogPath(logPath);
// 2 初始化调度中心地址和访问令牌
initAdminBizList(adminAddresses, accessToken);
// 3 初始化日志清理线程
JobLogFileCleanThread.getInstance().start(logRetentionDays);
// 4 初始化回调线程
TriggerCallbackThread.getInstance().start();
// 5 初始化调度器服务器
initEmbedServer(address, ip, port, appname, accessToken);
}
3.2.1 初始化日志
这里还是主要看一下第三步启动日志清理线程,这也算是一种最简单的定时任务,每天执行一次日志文件的清理
localThread = new Thread(new Runnable() {
@Override
public void run() {
while (!toStop) {
// 文件清理
。。。。。。。。
try {
TimeUnit.DAYS.sleep(1);
} catch (InterruptedException e) {
if (!toStop) {logger.error(e.getMessage(), e);}
}
}
logger.info(">>>>>>>>>>> xxl-job, executor JobLogFileCleanThread thread destory.");
}
});
localThread.setDaemon(true);
localThread.setName("xxl-job, executor JobLogFileCleanThread");
localThread.start();
}
3.2.2初始化调度器服务器
第五步是最关键的一步,启动netty服务端的核心代码
private void initEmbedServer(String address, String ip, int port, String appname, String accessToken) throws Exception {
// 进行端口探测,默认情况下9999端口,如果被占用会逐次加1
port = port>0?port: NetUtil.findAvailablePort(9999);
// 获取本机ip地址
ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();
// 拼接服务地址
if (address==null || address.trim().length()==0) {
String ip_port_address = IpUtil.getIpPort(ip, port);
address to registry , otherwise use ip:port if address is null
address = "http://{ip_port}/".replace("{ip_port}", ip_port_address);
}
// 启动netty服务器
embedServer = new EmbedServer();
embedServer.start(address, port, appname, accessToken);
}
由于过于重要,这里单独分一章节讲解EmbedServer类
四 netty服务处理
启动netty服务端也就是embedServer的start方法,我们一行一行的来解析
// 创建服务端对象
ServerBootstrap bootstrap = new ServerBootstrap();
// 设置EventLoopGroup,可以理解为线程池
// bossGroup处理连接,workerGroup负责处理读写
bootstrap.group(bossGroup, workerGroup)
// 设置使用的channel
.channel(NioServerSocketChannel.class)
// 设置处理器
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel channel) throws Exception {
channel.pipeline()
// 心跳处理器 90s没有读写操作执行其他handler的userEventTrigger的方法
.addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS))
// http编解码器
.addLast(new HttpServerCodec())
// HttpRequestDecoder会将消息分块,这里将其聚合成一个http对象
.addLast(new HttpObjectAggregator(5 * 1024 * 1024))
// 自定义处理器
.addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));
}
})
// 开启 SO_KEEPALIVE,类似于心跳检测
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 绑定端口,sync 阻塞,不阻塞的化下面的操作都不无法进行
ChannelFuture future = bootstrap.bind(port).sync();
// 注册执行器,向调度中心发送注册信息
startRegistry(appname, address);
// 一致阻塞
future.channel().closeFuture().sync();
重点关注这个xxljob自定义的处理器 EmbedHttpServerHandler,该处理器父类SimpleChannelInboundHandler,而父类又继承ChannelInboundHandlerAdapter,所以是 入站处理器 重写封装过的channelRead0()方法即可。
在这段代码的最后又调用ctx.writeAndFlush(response) 方法,又会依次执行出站处理器的方法(例如HttpServerCodec会对FullHttpResponse 进行http格式的编码)。
@Override
protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
// 解析请求
String requestData = msg.content().toString(CharsetUtil.UTF_8);
String uri = msg.uri();
HttpMethod httpMethod = msg.method();
boolean keepAlive = HttpUtil.isKeepAlive(msg);
String accessTokenReq = msg.headers().get(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN);
// 大概是防止调度任务执行过长,单独开一个线程处理
bizThreadPool.execute(new Runnable() {
@Override
public void run() {
// 处理各种api的请求
Object responseObj = process(httpMethod, uri, requestData, accessTokenReq);
// 转json
String responseJson = GsonTool.toJson(responseObj);
// 写入消息
writeResponse(ctx, keepAlive, responseJson);
}
});
}
private Object process(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) {
// 校验参数,略
。。。。。
// 处理请求
try {
if ("/beat".equals(uri)) {
return executorBiz.beat();
} else if ("/idleBeat".equals(uri)) {
IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class);
return executorBiz.idleBeat(idleBeatParam);
} else if ("/run".equals(uri)) {
TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
return executorBiz.run(triggerParam);
} else if ("/kill".equals(uri)) {
KillParam killParam = GsonTool.fromJson(requestData, KillParam.class);
return executorBiz.kill(killParam);
} else if ("/log".equals(uri)) {
LogParam logParam = GsonTool.fromJson(requestData, LogParam.class);
return executorBiz.log(logParam);
} else {
return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found.");
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
return new ReturnT<String>(ReturnT.FAIL_CODE, "request error:" + ThrowableUtil.toString(e));
}
}
private void writeResponse(ChannelHandlerContext ctx, boolean keepAlive, String responseJson) {
// 设置响应
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(responseJson, CharsetUtil.UTF_8)); // Unpooled.wrappedBuffer(responseJson)
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8"); // HttpHeaderValues.TEXT_PLAIN.toString()
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
if (keepAlive) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
// 将数据写入并刷出
ctx.writeAndFlush(response);
}
刚好对应了官网介绍的五种接口
/beat 调度中心检测执行器是否在线时使用
/idleBeat 调度中心检测指定执行器上指定任务是否忙碌(运行中)时使用
/run 触发任务执行
/kill 终止任务
/log 查看日志,滚动方式加载
具体接口文档详细可以去翻阅官网,执行器的所有请求都会在这里处理,然后根据路径分发给不同处理逻辑的方法
下一章挑选最重要的run方法进行分析
五 触发任务
我觉得解决方案和springmvc类似,把方法注册到map,然后根据传入的定时任务名称找到对象的method,反射调用。
事实上一切并没有我们想的那么简单。xxljob还要处理重复调用、日志记录、结果返回等等一系列问题
// 用来维护正在执行的 JobThread
// JobThread继承了thread
private static ConcurrentMap<Integer, JobThread> jobThreadRepository = new ConcurrentHashMap<Integer, JobThread>();
public static JobThread loadJobThread(int jobId){
JobThread jobThread = jobThreadRepository.get(jobId);
return jobThread;
}
@Override
public ReturnT<String> run(TriggerParam triggerParam) {
// 找到当前id,有没有已经注册的线程
JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId());
// 找到当前线程的处理器
IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null;
String removeOldReason = null;
// 获取任务类型(这里只分析BEAN类型)
GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType());
if (GlueTypeEnum.BEAN == glueTypeEnum) {
// 根据参数的处理器获得任务处理器
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) {
。。。
} else if (glueTypeEnum!=null && glueTypeEnum.isScript()) {
。。。
} else {
return new ReturnT<String>(ReturnT.FAIL_CODE, "glueType[" + triggerParam.getGlueType() + "] is not valid.");
}
/**
处理阻塞策略,有三类策略
SERIAL_EXECUTION("Serial execution"),串行
DISCARD_LATER("Discard Later"), 丢弃新的
COVER_EARLY("Cover Early");杀掉老线程,执行新的
**/
if (jobThread != null) {
ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerParam.getExecutorBlockStrategy(), null);
if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) {
// 判断一下当前任务是否在运行
// 如果在运行则啥也不干直接翻译一个
if (jobThread.isRunningOrHasQueue()) {
return new ReturnT<String>(ReturnT.FAIL_CODE, "block strategy effect:"+ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle());
}
} else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) {
// 设空 正在运行的线程
if (jobThread.isRunningOrHasQueue()) {
removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle();
jobThread = null;
}
} else {
}
}
// 如果没有找到该任务的旧线程
if (jobThread == null) {
// 创建一个新的jobThread并注册到jobThreadRepository,同样也是这个任务创建的核心
jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
}
// 把triggerParam详细信息放入当前线程
ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);
return pushResult;
}
private LinkedBlockingQueue<TriggerParam> triggerQueue;
public ReturnT<String> pushTriggerQueue(TriggerParam triggerParam) {
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;
}
这里主要是做了一下防重复任务,和任务阻塞策略的一个处理,核心还是registJobThread方法
// 可以理解为任务池
private static ConcurrentMap<Integer, JobThread> jobThreadRepository = new ConcurrentHashMap<Integer, JobThread>();
public static JobThread registJobThread(int jobId, IJobHandler handler, String removeOldReason){
// 创建一个JobThread类(继承了Thread)
JobThread newJobThread = new JobThread(jobId, handler);
// 开启线程执行run方法
newJobThread.start();
logger.info(">>>>>>>>>>> xxl-job regist JobThread success, jobId:{}, handler:{}", new Object[]{jobId, handler});
// 去jobThreadRepository任务池替换任务
JobThread oldJobThread = jobThreadRepository.put(jobId, newJobThread);
// 老任务kill掉
if (oldJobThread != null) {
oldJobThread.toStop(removeOldReason);
oldJobThread.interrupt();
}
return newJobThread;
}
下面将是本文的重中之重,之前的一切都可以是理解为逻辑校验,这里才是执行任务的源头
public class JobThread extends Thread{
private int jobId;
private IJobHandler handler;
// 任务队列
private LinkedBlockingQueue<TriggerParam> triggerQueue;
private Set<Long> triggerLogIdSet;
@Override
public void run() {
try {
// 调用处理器的init方法(注解上可以配置)
handler.init();
} catch (Throwable e) {
logger.error(e.getMessage(), e);
}
// execute
while(!toStop){
// 正在运行的标志位
running = false;
idleTimes++;
TriggerParam triggerParam = null;
ReturnT<String> executeResult = null;
try {
// 从任务队列获取任务
triggerParam = triggerQueue.poll(3L, TimeUnit.SECONDS);
if (triggerParam!=null) {
running = true;
idleTimes = 0;
triggerLogIdSet.remove(triggerParam.getLogId());
// 日志文件名 "logPath/yyyy-MM-dd/9999.log"
String logFileName = XxlJobFileAppender.makeLogFileName(new Date(triggerParam.getLogDateTime()), triggerParam.getLogId());
XxlJobFileAppender.contextHolder.set(logFileName);
ShardingUtil.setShardingVo(new ShardingUtil.ShardingVO(triggerParam.getBroadcastIndex(), triggerParam.getBroadcastTotal()));
if (triggerParam.getExecutorTimeout() > 0) {
// 如果配置了超时等待时间,或单独创建一个FutureTask
Thread futureThread = null;
try {
final TriggerParam triggerParamTmp = triggerParam;
FutureTask<ReturnT<String>> futureTask = new FutureTask<ReturnT<String>>(new Callable<ReturnT<String>>() {
@Override
public ReturnT<String> call() throws Exception {
return handler.execute(triggerParamTmp.getExecutorParams());
}
});
futureThread = new Thread(futureTask);
futureThread.start();
// get结果(该方法为阻塞方法,但是超时会抛出超时异常)
executeResult = futureTask.get(triggerParam.getExecutorTimeout(), TimeUnit.SECONDS);
} catch (TimeoutException e) {
。。。
} finally {
futureThread.interrupt();
}
} else {
// 在当前线程,立刻执行
executeResult = handler.execute(triggerParam.getExecutorParams());
}
} else {
if (idleTimes > 30) {
if(triggerQueue.size() == 0) {
// 如果长时间没有任务进来,就从注册的JobThread列表中移除当前JobThread
XxlJobExecutor.removeJobThread(jobId, "excutor idel times over limit.");
}
}
}
} catch (Throwable e) {
。。。
} finally {
。。。
}
}
}
// 调用销毁方法(注解上可配置)
try {
handler.destroy();
} catch (Throwable e) {
logger.error(e.getMessage(), e);
}
}
为了便于阅读,我这里删去了大量异常判断,只展示了重要的逻辑部分。接着看handler的execute方法
@Override
public ReturnT<String> execute(String param) throws Exception {
return (ReturnT<String>) method.invoke(target, new Object[]{param});
}
就简单的一行,反射调用,果然反射yyds。
六 总结
没啥好总结的,执行器部分逻辑还算清晰,但是关于netty的部分,虽然执行器的服务可能不是web服务,但我认为xxljob没有必要无脑的单独开一个服务器,即使netty很轻量。这里完全可以判断是不是web服务,如果是则共用一个端口,如果不是才考虑netty的方案,不知道作者是咋考虑的(ˉ▽ˉ;)...
下章将继续介绍xxljob的调度器篇。