一般定時任務使用的是基於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<Class<?>> 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<String> 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的方式關閉,這樣會收不到關閉信號無法執行鉤子程序。