任务调度 : XXL-JOB 客户端注册

2,214 阅读4分钟

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…

一. 前言

准备花三个篇幅来看看 XXL-JOB 的相关概念 , 这一篇主要看看 Client 端的请求流程 .

二. 使用

使用主要分为2个部分 :

  • 继承 IJobHandler 接口 , 实现 execute
  • 标注 @JobHandler(value = "testJobHandler") 接口
@JobHandler(value = "testJobHandler")
@Component
@Service
public class TestHandleService extends IJobHandler {


    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public ReturnT<String> execute(String s) throws Exception {
        logger.info("param is :{}", s);
        return null;
    }
}

服务端配置

image.png

三. 源码

下面来看一下 Job 源码的处理方式 ,对于 Spring 系列 ,是通过 XxlJobSpringExecutor 作为其执行器使用的 :

3.1 初始化操作

XxlJobSpringExecutor 是通过 ApplicationContextAware 来实现通知的 , 其启动的方法是在配置类中定义的 :

@Bean(initMethod = "start", destroyMethod = "destroy")
public XxlJobSpringExecutor xxlJobExecutor() {
    // 设置 AdminAddresses 和 AppName 和 IP 等
}

3.1.1 XxlJobSpringExecutor # start 部分 :

// C- XxlJobSpringExecutor 
public void start() throws Exception {

    // init JobHandler Repository -> 3.2 JobHandler 扫描
    initJobHandlerRepository(applicationContext);

    // refresh GlueFactory
    // 通过 type 类型创建 GlueFactory 或者 SpringGlueFactory
    GlueFactory.refreshInstance(1);


    // super start , 调用 XxlJobExecutor.start
    super.start();
}


3.1.2 XxlJobExecutor # start 部分 :

// C- XxlJobExecutor
public void start() throws Exception {

    // init logpath
    // 其中包括准备 logBasePath 和 glueSrcPath 以及 mkdir 创建路径
    XxlJobFileAppender.initLogPath(logPath);

    // init invoker, admin-client
    // Step 1 : adminAddresses.trim().split(",") -> 用逗号分隔集群
    // Step 2 : new XxlRpcReferenceBean 构建 AdminBiz
    // Step 3 : 添加到 List<AdminBiz> adminBizList 中
    initAdminBizList(adminAddresses, accessToken);


    // init JobLogFileCleanThread
    // 创建一个单独的线程处理 log 文件
    JobLogFileCleanThread.getInstance().start(logRetentionDays);

    // init TriggerCallbackThread -> 详见下方
    TriggerCallbackThread.getInstance().start();

    // init executor-server
    // 提供 9999 的默认端口 , 以及设置 IP
    port = port>0?port: NetUtil.findAvailablePort(9999);
    ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();
    
    // 
    initRpcProvider(ip, port, appName, accessToken);
}

3.1.3 TriggerCallbackThread 回调处理

作用 : 返回调用结果
创建 : 其中创建了2个线程 :

  • triggerCallbackThread
  • triggerRetryCallbackThread

public void start() {


    triggerCallbackThread = new Thread(new Runnable() {

        @Override
        public void run() {

            // while 死循环 , 直到结束信号
            while(!toStop){
                // 内部包含一个核心对象 : ReturnT<String> executeResult
                // executeResult = code + msg + content
                HandleCallbackParam callback = getInstance().callBackQueue.take();
                if (callback != null) {

                    // callback list param
                    List<HandleCallbackParam> callbackParamList = new ArrayList<HandleCallbackParam>();

                    // LinkedBlockingQueue<HandleCallbackParam> callBackQueue
                    // 此处从 queue 中一次性获取所有的数据的个数
                    int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList);
                    callbackParamList.add(callback);

                    // callback, will retry if error
                    if (callbackParamList!=null && callbackParamList.size()>0) {

                        // 调用 callback
                        doCallback(callbackParamList);
                    }
                }
 
            }

            // last callback
            List<HandleCallbackParam> callbackParamList = new ArrayList<HandleCallbackParam>();
            int drainToNum = getInstance().callBackQueue.drainTo(callbackParamList);
            if (callbackParamList!=null && callbackParamList.size()>0) {
                doCallback(callbackParamList);
            }
            logger.info(">>>>>>>>>>> xxl-job, executor callback thread destory.");

        }
    });
    triggerCallbackThread.setDaemon(true);
    triggerCallbackThread.setName("xxl-job, executor TriggerCallbackThread");
    triggerCallbackThread.start();


    // retry
    triggerRetryCallbackThread = new Thread(new Runnable() {
        @Override
        public void run() {
            while(!toStop){
                retryFailCallbackFile();
                TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
            }
            logger.info(">>>>>>>>>>> xxl-job, executor retry callback thread destory.");
        }
    });
    triggerRetryCallbackThread.setDaemon(true);
    triggerRetryCallbackThread.start();

}

doCallback

private void doCallback(List<HandleCallbackParam> callbackParamList){
    boolean callbackRet = false;
    // callback, will retry if error
    for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
        // callback 发起 RPC 远程调用
        ReturnT<String> callbackResult = adminBiz.callback(callbackParamList);
        if (callbackResult!=null && ReturnT.SUCCESS_CODE == callbackResult.getCode()) {
            callbackLog(callbackParamList, "<br>----------- xxl-job job callback finish.");
            callbackRet = true;
            break;
        } else {
            // 创建 callback log
            callbackLog(callbackParamList, "<br>----------- xxl-job job callback fail, callbackResult:" + callbackResult);
        }
    }
    if (!callbackRet) {
        appendFailCallbackFile(callbackParamList);
    }
}

3.1.4 initRpcProvider 注册服务

// C- XxlJobExecutor
private void initRpcProvider(String ip, int port, String appName, String accessToken) throws Exception {

    // init, provider factory
    String address = IpUtil.getIpPort(ip, port);
    
    // 准备远程调用属性
    Map<String, String> serviceRegistryParam = new HashMap<String, String>();
    serviceRegistryParam.put("appName", appName);
    serviceRegistryParam.put("address", address);

    
    // 初始化配置信息 , 包括使用 NettyHttpServer , 序列化方式 , ExecutorServiceRegistry 
    // NetEnum 包括四种常见的 : 
    // - NettyServer / NettyClient
    // - NettyHttpServer / NettyHttpClient
    // - MinaServer / MinaClient
    // - JettyServer / JettyClient
    xxlRpcProviderFactory = new XxlRpcProviderFactory();
    xxlRpcProviderFactory.initConfig(NetEnum.NETTY_HTTP, Serializer.SerializeEnum.HESSIAN.getSerializer(), ip, port, accessToken, ExecutorServiceRegistry.class, serviceRegistryParam);

    // add services -> 加入 Map<String, Object> serviceData 
    xxlRpcProviderFactory.addService(ExecutorBiz.class.getName(), null, new ExecutorBizImpl());

    // start -> Service 的注册
    xxlRpcProviderFactory.start();

}

远程注册 Service

// C- 
public void start() throws Exception {
   // start server
   serviceAddress = IpUtil.getIpPort(this.ip, port);
   server = netType.serverClass.newInstance();
   
   server.setStartedCallback(new BaseCallback() {    // serviceRegistry started
      @Override
      public void run() throws Exception {
         // start registry
         if (serviceRegistryClass != null) {
         
            // 构建 serviceRegistry 用于注册  
            serviceRegistry = serviceRegistryClass.newInstance();
            
            // ExecutorRegistryThread.getInstance().start(param.get("appName"), param.get("address"));
            // -> 3.3 发起实际注册流程 -> ExecutorServiceRegistry
            serviceRegistry.start(serviceRegistryParam);
            if (serviceData.size() > 0) {
               serviceRegistry.registry(serviceData.keySet(), serviceAddress);
            }
         }
      }
   });
   
   // 注册停止回调
   server.setStopedCallback(new BaseCallback() {     // serviceRegistry stoped
      @Override
      public void run() {
         // stop registry
         if (serviceRegistry != null) {
            if (serviceData.size() > 0) {
                // 移除加停止
               serviceRegistry.remove(serviceData.keySet(), serviceAddress);
            }
            // registryThread.interrupt();
            // registryThread.join();
            serviceRegistry.stop();
            serviceRegistry = null;
         }
      }
   });
   
   // 开启线程处理
   server.start(this);
}

Server 和 ServiceRegistry 类结构

xxl-job-Server.png

XXL-JOB-ServiceRegistry.png

3.2 注解的扫描

在 XxlJobSpringExecutor# initJobHandlerRepository 中对所有的 jobHandler 进行了扫描

private void initJobHandlerRepository(ApplicationContext applicationContext){

    // 通过 applicationContext 工具获取所有带 jobHandler 注解的类
    Map<String, Object> serviceBeanMap = applicationContext.getBeansWithAnnotation(JobHandler.class);

    if (serviceBeanMap!=null && serviceBeanMap.size()>0) {
        for (Object serviceBean : serviceBeanMap.values()) {
            // 这里就是为什么定义的 Job 任务需要实现 IJobHandler 接口
            if (serviceBean instanceof IJobHandler){
                // 获取注解携带的 name 信息
                String name = serviceBean.getClass().getAnnotation(JobHandler.class).value();
                IJobHandler handler = (IJobHandler) serviceBean;
                
                // jobHandlerRepository 中不可以存在重名的job
                if (loadJobHandler(name) != null) {
                    throw new RuntimeException("xxl-job jobhandler naming conflicts.");
                }
                
                // 将 Handler 加入 ConcurrentMap 中
                registJobHandler(name, handler);
            }
        }
    }
}


// jobHandlerRepository 实际是一个 Map
private static ConcurrentMap<String, IJobHandler> jobHandlerRepository = new ConcurrentHashMap<String, IJobHandler>();
public static IJobHandler registJobHandler(String name, IJobHandler jobHandler){
    return jobHandlerRepository.put(name, jobHandler);
}
public static IJobHandler loadJobHandler(String name){
    return jobHandlerRepository.get(name);
}


3.3 流程的处理

这一部分来看一下当前@JobHandler 被注册到 admin 的细节 , 其主要处理类为 ExecutorRegistryThread

// C- ExecutorServiceRegistry
ExecutorRegistryThread.getInstance().start(param.get("appName"), param.get("address"));


// C- ExecutorRegistryThread
public void start(final String appName, final String address){

    // 如果 appName 和 XxlJobExecutor.getAdminBizList() 为 null , 则直接 return
    
    registryThread = new Thread(new Runnable() {
        @Override
        public void run() {

            // registry 死循环
            while (!toStop) {
                try {
                    
                    // 准备 RegistryParam 对象进行注册处理
                    RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appName, address);
                    for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
                        // 发起流程注册
                        // client.asyncSend(finalAddress, xxlRpcRequest)
                        // TODO : XXL-JOB 的 RPC 调用也是个比较大的模块 , 后续专门看
                        ReturnT<String> registryResult = adminBiz.registry(registryParam);
                        if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
                            registryResult = ReturnT.SUCCESS;
                            logger.debug(">>>>>>>>>>> xxl-job registry success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                            break;
                        } else {
                            logger.info(">>>>>>>>>>> xxl-job registry fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                        }

                    }
                } catch (Exception e) {
                    // 打印异常日志
                }

                // while 循环中自动等待
                TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
            }

            // registry remove , 移除 service , 主要包含2件事
            - AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()
            - ReturnT<String> registryResult = adminBiz.registryRemove(registryParam);
            - registryResult = ReturnT.SUCCESS;
        }
    });
    registryThread.setDaemon(true);
    registryThread.setName("xxl-job, executor ExecutorRegistryThread");
    registryThread.start();
}

3.4 JobThread 的调用

TODO : 这里先不关注客户端回调的相关逻辑 , 下一篇文章专门看看

总结

作为开篇 , 挑了些软柿子捏 , 后面再深入更细化的层面 , xxl-job 是一个非常好用的任务调度平台 , 除了它提供的全面的功能外 , 代码和结构都非常清晰.

  • 开局会注册 service 到 server 中 , 有多种方式注册
  • 执行完成后通过 callback 回调 server

image.png