【任务调度系统第二篇】:XXL Job源码分析

写在前面

首先附上xxl job项目源码地址:https://github.com/xuxueli/xxl-job
本文章比较长,主要讲解思路为:

  • XXL JOB项目源码整体概括
  • xxl-job-admin的源码分析
  • 执行器代码分析
  • XXL Job需要改进的地方

一.XXL JOB项目源码整体概括

1. 源码整体概括说明

这个项目是作为工程开发的同学们很值得学习的一个开源项目。代码整体风格比较好,模块化清晰。代码逻辑遵行Web的MVC架构,采用Spring boot + Mybatis的框架组合来组织代码。
代码总体分为三部分:
一.xxl-job-core: 这是公共服务模块,比如提供RPC远程调度,线程管理等。从业务角度去分析这个模块是没有意义的,很容易一脑雾水,因为这个模块不是独立的服务,它只是为xxl-job-admin和xxl-job-executors-sample提供了功能模块。

二. xxl-job-admin: web交互的后台引擎,这里称为调度中心。主要负责下面几件事情:

  1. 负责web端交互:作为Web后台引擎,提供了登录权限管理,任务增删改查操作,执行器组管理,GLUE任务在线编辑,日志管理等
  2. 与MySQL数据库交互,把数据持久化。
  3. 提供RPC接口,供执行器注册,维持和执行器的心跳。
  4. 与quartz交互,把任务调度的事情交给quartz去做。

三. xxl-job-executors-sample。主要做以下两件事情:

  1. 执行器初始化,并且主动注册到调度中心那里去。
  2. bean的方式注入我们线下编辑好的任务。

整体架构图如下。后续章节会对细节进行展开阐述。
在这里插入图片描述

图1. 代码整体逻辑架构图

在这里插入图片描述
图2. xxl job的Web UI真实界面

2.分析该项目源码时一些必须的知识

磨刀不误砍材工,在正式深入分析这个项目之前,有些知识有必要预知下:
1.quartz的用法。
2.freemarker渲染前端界面的原理和用法。
3.java基本功,以及spring boot和mybatis相关框架知识。

2.1 quartz简单介绍

xxl job的任务调度是依赖于quartz的。 quartz可用于创建执行数十,数百甚至数十万个作业的简单或复杂的计划; 任务定义为标准Java组件的任务,可以执行任何可以对其进行编程的任何内容。我们先从quartz官网的一个例子说起:

// 第一步,定义任务类。这个class必须要实现Job接口的execute方法。
public class HelloJob implements Job {
   

    private static Logger _log = LoggerFactory.getLogger(HelloJob.class);
    public HelloJob() {
   
    }
    public void execute(JobExecutionContext context)
        throws JobExecutionException {
   
        _log.info("Hello World! - " + new Date());
    }

}

//2. 定义任务的执行逻辑,将任务和触发器绑定起来。 
public class SimpleExample {
   

  public void run() throws Exception {
   

    log.info("------- 初始化----------------------");
 
    // 首先,我们得到一个scheduler实例
    SchedulerFactory sf = new StdSchedulerFactory();
    Scheduler sched = sf.getScheduler();

    log.info("------- 初始化完成 -----------");

    // computer a time that is on the next round minute
    Date runTime = evenMinuteDate(new Date());

    log.info("------- 调度任务 -------------------");

    // define the job and tie it to our HelloJob class
    JobDetail job = JobBuilder.newJob(HelloJob.class).withIdentity("job1", "group1").build();

    // Trigger the job to run on the next round minute
    Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "group1").startAt(runTime).build();
 
    // 告诉quartz利用trigger触发器来调度job
    sched.scheduleJob(job, trigger);
    log.info(job.getKey() + " will run at: " + runTime);

    // Start up the scheduler (nothing can actually run until the
    // scheduler has been started)
    sched.start();

    log.info("------- 任务已经已经启动了 -----------------");

    // wait long enough so that the scheduler as an opportunity to
    // run the job!
    log.info("------- Waiting 65 seconds... -------------");
    try {
   
      // wait 65 seconds to show job
      Thread.sleep(65L * 1000L);
      // executing...
    } catch (Exception e) {
   
      //
    }

    // shut down the scheduler
    log.info("------- 调度关闭 ---------------------");
    sched.shutdown(true);
    log.info("------- 关闭完成 -----------------");
  }
   
  public static void main(String[] args) throws Exception {
   

    SimpleExample example = new SimpleExample();
    example.run();

  }
}

从上面的demo可以看出quartz的关键API:

  • Scheduler - 进行作业调度的主要接口.
  • Job - 作业接口,编写自己的作业需要实现,如例子中的HelloJob
  • JobDetail - 作业的详细信息,除了包含作业本身,还包含一些额外的数据。
  • Trigger - 作业计划的组件-作业何时执行,执行次数,频率等。
  • JobBuilder - 建造者模式创建 JobDetail实例.
  • TriggerBuilder - 建造者模式创建 Trigger 实例.
  • QuartzSchedulerThread 继承Thread 主要的执行任务线程

从上面的几个接口,可以看到quartz设计非常精妙,将作业和触发器分开设计,同时调度器完成对作业的调度。
整个执行过程可以概括如下:

  1. 从StdSchedulerFactory获取scheduler
  2. 创建JobDetail
  3. 创建Trigger
  4. scheduler.scheduleJob()将任务和触发器绑定起来

所以quartz的核心元素可以表示为如下图:
在这里插入图片描述
图3. quartz内部核心模块关系图

quartz不是以定时器的方式去执行任务的,而是通过线程池去完成。配置文件quartz.properties配置了线程池相关的参数。在quartz中,有两类线程,Scheduler调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bT3CYQD3-1571059534531)(https://cdn.nlark.com/yuque/0/2018/png/171710/1540885992917-894fa2a9-f022-4a35-80fb-18a84507f89c.png “”)]

图4. quartz的线程视图

调度线程主要有两个:执行常规调度的线程,和执行misfiredtrigger的线程。常规调度线程轮询存储的所有trigger,如果有需要触发的trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该trigger关联的任务。Misfire线程是扫描所有的trigger,查看是否有misfiredtrigger,如果有的话根据misfire的策略分别处理(fire now 或者 wait for the next fire)。

quartz内部的数据是存入数据库的,总共有12张表。Quartz集群中,独立的Quartz节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一Quartz应用的。 到此,我认为quartz的核心要点应该介绍完了。

2.2 freemarker前端渲染模板简介

freemarker是一个java模板引擎。是一种基于模板和要改变的数据,并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。类似于JSP,volecity。这里不细说,有这个概念就好了。

2.3 java基本功修炼

xxl job的源码阅读,需要一定的java工程功底。特别要熟悉下spring boot, mybatis框架。

二. xxl-job-admin的源码分析

xxl-job-admin是项目的核心,称为调度中心,也是一个典型的web项目架构。通常对于一个web程序来说,我们分析时,主要是关注两件事情:第一,这个程序在初始化(也就是程序启动的时候)干了哪些事情;第二,程序的Restful接口分析,这个是Web项目最大的主线。下面的分析我们也主要是从这两点分别展开。

1. 调度中心初始化

JVM执行一个java程序时,会经历编译,加载,分配内存和执行等过程。spring boot采用了的bean方式初始化了一些对象,这些对象包括了数据库连接池,前端界面渲染的引擎,配置文件读取,quartz调度引擎,拦截器等等,这些对象一旦初始化,就会从JVM的方法区里实例化到堆内存里面去,可以供进程后续的调用。这里有个和我们业务直接相关的bean初始化,代码如下:

<!--classpath:applicationcontext-xxl-job-admin-xml-->
<bean id="xxlJobDynamicScheduler" class="com.xxl.job.admin.core.schedule.XxlJobDynamicScheduler" init-method="init" destory-method="destory">
   <property name="scheduler" ref="quartzScheduler" />
   <property name="accessToken" value="${xxl.job.accessToken" />
</bean>

这个XxlJobDynamicScheduler类在初始化化时,执行了init方法。我们来重点分析下这个init方法干了哪些事情。

public void init() throws Exception {
   
  // 1. 调度中心注册守护线程,就是一直守护着执行器的注册,维持着和执行器之间的心跳
  JobRegistryMonitor.getInstance.start();
  
  // 2. 任务失败处理的守护线程
  JobFailMonitorHelper.getInstance().start();
  // 3. 初始化本地调度中心服务
  NetComServerFactory.putService(AdminBiz.class, XxlJobDynamicScheduler.adminBiz);
  NetComServerFactory.setAcessToken(accessToken);
  
  // 4.国际化 
  initI18n();
  Assert.notNull(scheduler, "quartz scheduler is null");
  logger.info(">>>>>>> init xxl-job admin success");
}
  1. JobRegistryMonitor.getInstance.start()是开启了一个单独的线程,这个线程每30s去轮训一下数据库。如果某个执行器的注册信号(也叫作心跳)在近90s内没有写入数据库表XXL_JOB_QRTZ_TRIGGER_REGISTRY,那么调度中心就认为这个执行器已经死掉。然后会更新数据库表XXL_JOB_QRTZ_TRIGGER_GROUP表,使每个执行器组,只保留活着的执行器。这里的执行器组是根据调度中心来区分的。每个执行器组(这个有可能是一台,也有可能是一个集群)都有一个唯一的appName,执行器向调度中心注册时就是通过这个appName标志来区分是属于哪个执行器组的。
  2. JobFailMonitorHelper.getInstance().start()是一个失败任务处理的守护线程。这个线程是每隔10秒执行一下逻辑。数据库表XXL_JOB_QRTZ_TRIGGER_LOG里存着每个任务每次的执行记录,这里面记录着任务的执行状态。如果某条日志记录的处理状态码为500,那么这条执行记录是以失败告终的。那么失败守护线程就会根据这个任务的executorFailRetryCount(失败重试次数)是否大于零(这个参数是前端新增任务时配置的),如果大于零,会去尝试再执行下这个任务。并且相应地在数据库里把该条执行日志里的executorFailRetryCount值减1。最后发出失败告警。
  3. 初始化本地的调度中心的服务Map,以及accessToken值。调度中心实例用HaspMap对象存了起来。
  4. 国际化。支持中文和英文展示。

所以总的来说,这里主要是初始化了两个守护线程。一个是维持和执行器之间心跳的线程,一个是任务执行失败重试的线程。

2. Web MVC逻辑分析

Controller层是我们理解后台逻辑的入口,com.xxl.job.admin.controller包里面中共包含了六大模块:权限登录模块,调度中心和执行器通信的RPC模块,GLUE任务编辑模块,执行器管理模块,任务操作模块和任务日志管理模块。从用户正常的交互角度分析,这些模块是有先后顺序的。用户首先是通过账户密码登录系统,然后查看调度中心里有没有已经自动注册上的执行器,如果没有,那么需要手动添加执行器。后续就可以创建任务了。任务创建时,GLUE类型的任务可以在线编辑任务逻辑代码的。任务确认创建好了之后,可以手动即席执行,还可以配置cron表达式进行周期调度执行。最后通过日志界面,查看每个任务的执行逻辑。所以本节也会根据这个先后顺序来介绍每个模块的具体逻辑。

2.1. 权限登录模块

程序的配置文件里配置了初始化的username为admin,password为123456。spring boot的xml配置文件里配置了两个拦截器。具体如下:

<mvc:interceptors>
   <mvc:interceptor>
     <mvc:mapping path="/**" />
     <bean class="com.xxl.job.admin.co
  • 11
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值