一般定时任务使用的是基于quartz或者spring-scheduler的,能够满足大部分的开发需求。但是像手动执行一次,执行情况监测,进程阻塞停止等维护需求就显得无能为力了。无意间在gitee.com上发现了一个很好满足以上需求的项目,来自许雪里开源的一个轻量级分布式任务调度平台xxl-job。gitee地址:https://gitee.com/xuxueli0323/xxl-job,主页为:http://www.xuxueli.com/xxl-job/。本文只触及到其简单的功能使用,更多功能和更深层次的理解请参考源码。
通过其demo使用和源码的梳理,基本搞明白了其运行原理。其主要分为任务执行器和任务调度器。任务执行器接收调度指令,执行相应的方法。任务调度器根据事先设定的每个job handler 的cron表达式调度任务执行器。如果调度失败可以根据配置再调度其他的执行器(集群模式下)。在集群模式下,相同appname的多个执行器组成一个执行器集群,Handler与执行器集群之间通过appname绑定。一个执行器相当于一个runner实体,一个runner可以有多个Handler(job),他们都由调度器管理,在调度器中指定Handler与执行器之间的关系。
执行器项目一般集成到自己的项目中,调度器是一个后台管理项目,二者分别部署到两个主机,通过rpc交互(xxl-rpc):调度器通过rpc的9999端口调用执行器,执行器回调走8080http端口调用调度中心。任务调度中心通过数据库管理执行器和job等实体信息,所以需要一个数据库。直接通过
java -jar xxl-job-admin-2.1.1-SNAPSHOT.jar &
的方式运行调度中心。配置执行器和任务。
路由策略
执行器参考其示例项目,引入xxl-job-core,初始化XxlJobExecutor,注册jobhandler即可。
我们以前的项目是基于quartz的,框架基于vertx,所以选择了FrameLess这种集成方式。
Set> jobHandlerClasses = ClassUtil.scanPackageBySuper("xx.xx.xxx.xxx.package", true, IJobHandler.class);
for (Class> jobHandlerClass : jobHandlerClasses) {
String name = StrUtil.lowerFirst(jobHandlerClass.getSimpleName());
if(jobHandlerClass.isAnnotationPresent(JobHandler.class)){
String value = jobHandlerClass.getAnnotation(JobHandler.class).value();
if(!"".equals(value)){
name = value;
}
}
IJobHandler jobHandler = BeanUtil.newInstance(jobHandlerClass);
XxlJobExecutor.registJobHandler(name , jobHandler);
}
// load executor prop
//Properties xxlJobProp = loadProperties("xxl-job-executor.properties");
Prop prop = PropertiesUtils.use(CONFIG_PATH + "xxl-job-executor.properties");
// init executor
xxlJobExecutor = new XxlJobExecutor();
xxlJobExecutor.setAdminAddresses(prop.get("xxl.job.admin.addresses"));
xxlJobExecutor.setAppName(prop.get("xxl.job.executor.appname"));
xxlJobExecutor.setIp(prop.get("xxl.job.executor.ip"));
xxlJobExecutor.setPort(prop.getInt("xxl.job.executor.port"));
xxlJobExecutor.setAccessToken(prop.get("xxl.job.accessToken"));
xxlJobExecutor.setLogPath(prop.get("xxl.job.executor.logpath"));
xxlJobExecutor.setLogRetentionDays(prop.getInt("xxl.job.executor.logretentiondays"));
// start executor
try {
xxlJobExecutor.start();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
以前的job不动,重新写jobhandler,然后将以前的job关闭,注册这个新的handler到调度器中。jobhandler中调用以前的service方法,先改几个不太重要的job,比如清理类的job,执不执行和多次执行都没啥影响的,运行无误之后再迁移其他所有的job。如此,可以非常平滑地迁移,迁移的风险也降到最低。job和jobhandler之间其实相当于两个马甲,都是调用service来完成具体的事情,从此也能看出代码规范的重要性。
public class DemoJobHandler extends IJobHandler {
@Override
public ReturnT execute(String param) throws Exception {
XxlJobLogger.log("XXL-JOB, Hello World.");
for (int i = 0; i < 5; i++) {
XxlJobLogger.log("beat at:" + i);
TimeUnit.SECONDS.sleep(2);
}
xxxxx.service.doSomething();
return SUCCESS;
}
}
FrameLess这种集成方式有一个非常重要的问题,就是如何保持程序的不退出?
第一种方式:利用quartz的job线程,即至少运行一个job,这种方式比较简单,但是不够优雅。
第二种方式:写一个hold住程序的类,jvm退出的两个理由是1)不再拥有前台进程和2)程序调用了exit,所以只要保证程序有一个前台进程,jvm就不会退出。
/**
* 使程序不退出,保证至少一个前台进程
* @see https://dubbo.apache.org/zh-cn/blog/spring-boot-dubbo-start-stop-analysis.html
* @author xiongshiyan at 2019/10/16 , contact me with email [email protected] or phone 15208384257
*/
public class HoldProcessor {
private volatile boolean stopAwait = false;
/**
* Thread that currently is inside our await() method.
*/
private volatile Thread awaitThread = null;
/**
* 开始等待
*/
public void startAwait(){
Thread awaitThread = new Thread(this::await,"hold-process-thread");
awaitThread.setContextClassLoader(getClass().getClassLoader());
//这一步很关键,保证至少一个前台进程
awaitThread.setDaemon(false);
awaitThread.start();
}
/**
* 停止等待,退出程序,一般放在shutdown hook中执行
* @see Runtime#addShutdownHook(Thread)
*/
public void stopAwait() {
//此变量
stopAwait=true;
Thread t = awaitThread;
if (null != t) {
t.interrupt();
try {
t.join(1000);
} catch (InterruptedException e) {
// Ignored
}
}
}
private void await(){
try {
awaitThread = Thread.currentThread();
while(!stopAwait) {
try {
TimeUnit.SECONDS.sleep(10);
} catch( InterruptedException ex ) {
// continue and check the flag
}
}
} finally {
awaitThread = null;
}
}
}
FrameLessXxlJobConfig.getInstance().initXxlJobExecutor();
HoldProcessor holdProcessor = new HoldProcessor();
holdProcessor.startAwait();
logger.info("程序开始等待");
Runtime.getRuntime().addShutdownHook(new Thread(()->{
logger.info("收到kill 信号,执行清理程序");
//在关闭的时候释放资源
FrameLessXxlJobConfig.getInstance().destroyXxlJobExecutor();
holdProcessor.stopAwait();
}));
参考:https://dubbo.apache.org/zh-cn/blog/spring-boot-dubbo-start-stop-analysis.html
基于以上的原因,程序不要使用kill -9 pid的方式关闭,这样会收不到关闭信号无法执行钩子程序。