XxlJob任务调度平台的执行器管理:上下线机制详解~

前言

在实际开发中,我们往往需要某些特定时刻执行一些任务,例如: 早上十点进行push推送凌晨2点刷新一批数据每隔5分钟进行失败补偿等等。

这时候我们就需要用到定时任务来完成上述任务~

业界的定时任务选型有很多,例如👇🏻

单机: jdk自带的ScheduledThreadPoolExecutor、Timer、开源框架

分布式: Quartzelastic-jobxxljob

本文来带大家了解的就是 xxl-job~


简单了解

xxlJob架构分为两大模块,即调度中心执行器

简单理解: 执行器即我们的服务启动后注册到调度中心,调度中心管理着所有执行器、任务,根据任务触发时间点下发到执行器执行。

本文来了解的就是执行器的注册与注销原理


执行器注册

执行器发起注册

xxjob官方springboot案例中,我们可以看到定义了一个XxlJobConfig配置类,同时在这个配置类中创建了XxlJobSpringExecutor这个bean,并且传入了xxljob admin地址、appname(执行器的名称)等信息

我们进入XxlJobSpringExecutor可见,它实现了SmartInitializingSingleton、DisposableBean,并重写了afterSingletonsInstantiated、destroy方法,其中👇🏻

afterSingletonsInstantiated 在所有单例 bean 都初始化完成以后进行调用

destroy在服务注销时会进行调用

java 复制代码
public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, SmartInitializingSingleton, DisposableBean {

  // start
  @Override
  public void afterSingletonsInstantiated() {

    // init JobHandler Repository
    /*initJobHandlerRepository(applicationContext);*/

    // 初始化并注册任务方法
    initJobHandlerMethodRepository(applicationContext);

    // refresh GlueFactory
    GlueFactory.refreshInstance(1);

    try {
      // 开始执行
      super.start();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  // destroy
  @Override
  public void destroy() {
    super.destroy();
  }
}

关于initJobHandlerMethodRepository方法,其源码比较简单,就不深究了

具体逻辑是拿到Spring容器里所有bean,挨个遍历,看看bean中是否有方法是被@XxlJob注解修饰的,如果存在这样的方法,最终将其封装为MethodJobHandler,以jobhandler namekeyMethodJobHandler value添加到ConcurrentMap中进行维护


注册完任务,咱们接着来看start逻辑,咱们重点了解initEmbedServer逻辑

java 复制代码
public class XxlJobExecutor  {

  // ---------------------- start + stop ----------------------
  public void start() throws Exception {

    // init logpath
    XxlJobFileAppender.initLogPath(logPath);

    // 初始化xxljob admin地址,可能存在多个节点,根据,分隔
    initAdminBizList(adminAddresses, accessToken);

    // init JobLogFileCleanThread
    JobLogFileCleanThread.getInstance().start(logRetentionDays);

    // init TriggerCallbackThread
    TriggerCallbackThread.getInstance().start();

    // 向调度中心发起注册
    initEmbedServer(address, ip, port, appname, accessToken);
  }
}
java 复制代码
private void initEmbedServer(String address, String ip, int port, String appname, String accessToken) throws Exception {

  // fill ip port
  port = port > 0 ? port : NetUtil.findAvailablePort(9999);
  ip = (ip != null && ip.trim().length() > 0) ? ip : IpUtil.getIp();

  // generate address
  if (address == null || address.trim().length() == 0) {
    String ip_port_address = IpUtil.getIpPort(ip, port);   // registry-address:default use address to registry , otherwise use ip:port if address is null
    address = "http://{ip_port}/".replace("{ip_port}", ip_port_address);
  }

  // accessToken
  if (accessToken == null || accessToken.trim().length() == 0) {
    logger.warn(">>>>>>>>>>> xxl-job accessToken is empty. To ensure system security, please set the accessToken.");
  }

  // start
  embedServer = new EmbedServer();
  embedServer.start(address, port, appname, accessToken);
}

initEmbedServer中先简单做了下参数处理,如果没有指定本机address、port则会进行获取

随后创建了EmbedServer,并进行start

java 复制代码
public class EmbedServer {
  private ExecutorBiz executorBiz;
  private Thread thread;

  public void start(final String address, final int port, final String appname, final String accessToken) {
    executorBiz = new ExecutorBizImpl();
    
    thread = new Thread(new Runnable() {
      @Override
      public void run() {
        // param
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ThreadPoolExecutor bizThreadPool = new ThreadPoolExecutor(
          0,
          200,
          60L,
          TimeUnit.SECONDS,
          new LinkedBlockingQueue<Runnable>(2000),
          new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
              return new Thread(r, "xxl-job, EmbedServer bizThreadPool-" + r.hashCode());
            }
          },
          new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
              throw new RuntimeException("xxl-job, EmbedServer bizThreadPool is EXHAUSTED!");
            }
          });
        try {
          
          // 创建netty server
          ServerBootstrap bootstrap = new ServerBootstrap();
          bootstrap.group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<SocketChannel>() {
              @Override
              public void initChannel(SocketChannel channel) throws Exception {
                channel.pipeline()
                  .addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS))  // beat 3N, close if idle
                  .addLast(new HttpServerCodec())
                  .addLast(new HttpObjectAggregator(5 * 1024 * 1024))  // merge request & reponse to FULL
                  .addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));
              }
            })
            .childOption(ChannelOption.SO_KEEPALIVE, true);

          // bind
          ChannelFuture future = bootstrap.bind(port).sync();

          logger.info(">>>>>>>>>>> xxl-job remoting server start success, nettype = {}, port = {}", EmbedServer.class, port);

          // 发起注册
          startRegistry(appname, address);

          // wait util stop
          future.channel().closeFuture().sync();

        } catch (InterruptedException e) {
          logger.info(">>>>>>>>>>> xxl-job remoting server stop.");
        } catch (Exception e) {
          logger.error(">>>>>>>>>>> xxl-job remoting server error.", e);
        } finally {
          // stop
          try {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
          } catch (Exception e) {
            logger.error(e.getMessage(), e);
          }
        }
      }
    });
    thread.setDaemon(true);    // daemon, service jvm, user thread leave >>> daemon leave >>> jvm leave
    thread.start();
  }
}

大致扫一眼,虽然看着start方法中代码挺多的,但其中无非也就两件事,创建netty serverstart registry

继续看注册,最终也是来到ExecutorRegistryThread中的start方法,其中管理着执行器的注册与注销逻辑

先看注册,注册逻辑很简单,遍历所有admin节点,挨个注册上去

java 复制代码
public class ExecutorRegistryThread {
  private static Logger logger = LoggerFactory.getLogger(ExecutorRegistryThread.class);

  private static ExecutorRegistryThread instance = new ExecutorRegistryThread();
  public static ExecutorRegistryThread getInstance(){
    return instance;
  }

  private Thread registryThread;
  private volatile boolean toStop = false;
  
  public void start(final String appname, final String address){
    // ......
    
    registryThread = new Thread(new Runnable() {
      @Override
      public void run() {

        // 发起注册
        while (!toStop) {
          try {
            RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
            // todo 前面提到了,admin可能存在多个节点,这里就遍历,注册到每一个节点上去
            for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
              try {
                // todo 发起注册请求
                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) {
                logger.info(">>>>>>>>>>> xxl-job registry error, registryParam:{}", registryParam, e);
              }

            }
          } catch (Exception e) {
            if (!toStop) {
              logger.error(e.getMessage(), e);
            }

          }

          try {
            if (!toStop) {
              TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
            }
          } catch (InterruptedException e) {
            if (!toStop) {
              logger.warn(">>>>>>>>>>> xxl-job, executor registry thread interrupted, error msg:{}", e.getMessage());
            }
          }
        }

        // ..... 注销

      }
    });
    registryThread.setDaemon(true);
    registryThread.setName("xxl-job, executor ExecutorRegistryThread");
    registryThread.start();
  }
}

最终发起http post请求,调用registry接口,进行注册。


调度中心处理注册

上面我们了解了执行器发起注册逻辑,最终是发起post请求调用api/registry接口

则来到JobApiController#api

相比于执行器发起注册 的逻辑,调度中心处理注册的逻辑就简单很多了

  1. 参数校验
  2. 异步完成注册
    1. 先进行更新操作
    2. 如果操作行数 < 1说明记录不存在,执行插入操作 ,写入到xxl_job_registry表中,完成注册
  3. 直接返回注册成功响应
java 复制代码
// com.xxl.job.admin.core.thread.JobRegistryHelper#registry
public ReturnT<String> registry(RegistryParam registryParam) {

  // 参数校验
  if (!StringUtils.hasText(registryParam.getRegistryGroup())
      || !StringUtils.hasText(registryParam.getRegistryKey())
      || !StringUtils.hasText(registryParam.getRegistryValue())) {
    return new ReturnT<String>(ReturnT.FAIL_CODE, "Illegal Argument.");
  }

  // 异步写入数据库完成注册
  registryOrRemoveThreadPool.execute(new Runnable() {
    @Override
    public void run() {
      // 先进行更新操作
      int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
      if (ret < 1) {
        // 操作行数 < 1,说明记录不存在,则执行插入操作,写入到xxl_job_registry表中
        XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());

        // fresh
        freshGroupRegistryInfo(registryParam);
      }
    }
  });

  // 直接返回成功
  return ReturnT.SUCCESS;
}

执行器注销

执行器注销分为主动注销被动注销

主动注销很好理解,例如服务发布,会用新节点替换旧节点,那么旧节点需要告诉调度中心我要下线了,请把我注销掉,然后新节点再主动注册到调度中心,这样任务调度就会调度到新节点执行。

像这种经典的client server通信,那么必不可少的就是探活机制,当探活失败时,调度中心会主动注销掉client,那么对于client来说就是被动注销


主动注销

回到最开始,我们了解到XxlJobSpringExecutor是实现了DisposableBean接口的,当服务下线时,会回调destroy方法

java 复制代码
public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, SmartInitializingSingleton, DisposableBean {

  // destroy
  @Override
  public void destroy() {
    super.destroy();
  }
}
java 复制代码
// com.xxl.job.core.executor.XxlJobExecutor#destroy
public void destroy(){
  // 注销netty server
  stopEmbedServer();

  // 停止所有job线程
  if (jobThreadRepository.size() > 0) {
    for (Map.Entry<Integer, JobThread> item: jobThreadRepository.entrySet()) {
      JobThread oldJobThread = removeJobThread(item.getKey(), "web container destroy and kill the job.");
      // 如果存在正在执行的job thread,则等待其执行完毕
      if (oldJobThread != null) {
        try {
          oldJobThread.join();
        } catch (InterruptedException e) {
          logger.error(">>>>>>>>>>> xxl-job, JobThread destroy(join) error, jobId:{}", item.getKey(), e);
        }
      }
    }
    
    // 清空 jobThread
    jobThreadRepository.clear();
  }

  // 清空jobhandler
  jobHandlerRepository.clear();


  // ......
}

stopEmbedServer最终也还是来到了前面提到过的ExecutorRegistryThread#toStop中,toStop标识设置为true,打断镔铁同步等待registryThread执行完毕

registryThread#run方法中,当toStop = true则跳出循环,向所有admin节点发起注销请求

java 复制代码
public class ExecutorRegistryThread {

  private volatile boolean toStop = false;

  public void toStop() {
    toStop = true;

    // interrupt and wait
    if (registryThread != null) {
      registryThread.interrupt();
      try {
        registryThread.join();
      } catch (InterruptedException e) {
        logger.error(e.getMessage(), e);
      }
    }

  }

  public void start(final String appname, final String address){
    registryThread = new Thread(new Runnable() {
      @Override
      public void run() {

        while(!toStop) {
          // ..... execute register
        }

        // registry remove
        try {
          RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
          // todo 遍历所有admin节点,一个一个发起注销请求
          for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
            try {
              // todo 发起注销请求
              ReturnT<String> registryResult = adminBiz.registryRemove(registryParam);
              if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
                registryResult = ReturnT.SUCCESS;
                logger.info(">>>>>>>>>>> xxl-job registry-remove success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                break;
              } else {
                logger.info(">>>>>>>>>>> xxl-job registry-remove fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
              }
            } catch (Exception e) {
              if (!toStop) {
                logger.info(">>>>>>>>>>> xxl-job registry-remove error, registryParam:{}", registryParam, e);
              }

            }

          }
        } catch (Exception e) {
          if (!toStop) {
            logger.error(e.getMessage(), e);
          }
        }
        logger.info(">>>>>>>>>>> xxl-job, executor registry thread destroy.");

      }
    });
    registryThread.setDaemon(true);
    registryThread.setName("xxl-job, executor ExecutorRegistryThread");
    registryThread.start();
  }
}

同注册一样,只不过这次是请求的registryRemove接口

JobApiController处理注销请求,本质上就是从xxl_job_registry表中删除记录


被动注销

调度中心init的时候,会开启一个registryMonitorThread线程,其中每隔30s会查询超过90s没有更新的执行器节点记录(认为探活失败), 查出来后会直接移除掉

在执行器启动的时候会向调度中心发起注册

之后每隔30s会再次发起注册,此时就会去更新节点在xxl_job_registryupdate_time,这样一样就能维持探活,节点就不会被移除


总结

通过本文,我们了解到了xxlJob执行器的注册、注销逻辑。

spring的环境下,利用spring留下的扩展接口,将执行器的节点信息注册到调度中心, 注销机制同理,同时调度中心执行器之间建立心跳机制,保证任务的正常调度。

我是 Code皮皮虾 ,会在以后的日子里跟大家一起学习,一起进步! 觉得文章不错的话,可以在 掘金 关注我,这样就不会错过很多技术干货啦~

相关推荐
新时代苦力工10 分钟前
处理对象集合,输出Map<String, Map<String, List<MyObject>>>格式数据,无序组合键处理方法
java·数据结构·list
极客智谷12 分钟前
Spring AI应用系列——基于Alibaba DashScope的聊天记忆功能实现
人工智能·后端
极客智谷13 分钟前
Spring AI应用系列——基于Alibaba DashScope实现功能调用的聊天应用
人工智能·后端
RJiazhen13 分钟前
5分钟让你的服务接入AI——用 API Auto MCP Server 实现大模型与后端系统的无缝对话
后端·开源·mcp
前端付豪16 分钟前
2、ArkTS 是什么?鸿蒙最强开发语言语法全讲解(附实操案例)
前端·后端·harmonyos
前端付豪19 分钟前
8、鸿蒙动画开发实战:做一个会跳舞的按钮!(附动效示意图)
前端·后端·harmonyos
前端付豪20 分钟前
3、构建你的第一个鸿蒙组件化 UI 页面:实现可复用的卡片组件(附实战代码)
前端·后端·harmonyos
Java水解20 分钟前
Feign结构与请求链路详解及面试重点解析
后端
前端付豪22 分钟前
7、打造鸿蒙原生日历组件:自定义 UI + 数据交互(附实操案例与效果图)
前端·后端·harmonyos
niesiyuan00023 分钟前
MAC如何安装多版本jdk(以8,11,17为例)
java