一文搞懂java中的分布式定时任务框架 - xxl-job

阅读收获

:heavy_check_mark:1. 了解常用的分布式应用定时任务框架

:heavy_check_mark:2. 掌握xxl-job定时任务框架搭建及使用

常用的分布式任务调度系统

  • xxl-job : 是大众点评员工徐雪里于2015年发布的分布式任务调度平台,是一个轻量级分布式任务调度框架,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。
  • Quartz :Java事实上的定时任务标准。但Quartz关注点在于定时任务而非数据,并无一套根据数据处理而定制化的流程。虽然Quartz可以基于数据库实现作业的高可用,但缺少分布式并行调度的功能
  • TBSchedule :阿里早期开源的分布式任务调度系统。代码略陈旧,使用timer而非线程池执行任务调度。众所周知,timer在处理异常状况时是有缺陷的。而且TBSchedule作业类型较为单一,只能是获取/处理数据一种模式。还有就是文档缺失比较严重
  • elastic-job(E-Job) :当当开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,实现任务高可用以及分片,目前是版本2.15,并且可以支持云开发
  • Saturn :是唯品会自主研发的分布式的定时任务的调度平台,基于当当的elastic-job 版本1开发,并且可以很好的部署到docker容器上。

image.png

  • 共同点:
    • E-Job和X-job都有广泛的用户基础和完整的技术文档,都能满足定时任务的基本功能需求。
  • 不同点
    • X-Job 侧重的业务实现的简单和管理的方便,学习成本简单,失败策略和路由策略丰富。推荐使用在“用户基数相对少,服务器数量在一定范围内”的情景下使用
    • E-Job 关注的是数据,增加了弹性扩容和数据分片的思路,以便于更大限度的利用分布式服务器的资源。但是学习成本相对高些,推荐在“数据量庞大,且部署服务器数量较多”时使用

xxl-job

设计思想:

  • 将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。
  • 将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。
  • 因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性;
  • 本文使用版本为2.3.0

1.下载 源码

image.png

2. 初始化 数据库

  • 执行tables_xxl_job.sql文件初始化
  • 调度中心支持集群部署,集群情况下各节点务必连接同一个mysql实例;
  • 如果mysql做主从,调度中心集群节点务必强制走主库;
    • xxl_job_lock:任务调度锁表;
    • xxl_job_group:执行器信息表,维护任务执行器信息;
    • xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等;
    • xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;
    • xxl_job_log_report:调度日志报表:用户存储XXL-JOB任务调度日志的报表,调度中心报表功能页面会用到;
    • xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;
    • xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;
    • xxl_job_user:系统用户表;

3. 配置部署 调度中心

  • 调度中心项目:xxl-job-admin
  • 作用: 统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。

3.1 主要修改配置:

  • 修改数据源
  • 修改报警邮箱
  • 调度中心通讯TOKEN
### xxl-job, datasource
spring.datasource.url=jdbc:mysql://localhost:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

### xxl-job, email
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.from=xxx@qq.com
spring.mail.password=你的mail token
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory

### 调度中心通讯TOKEN [选填]:非空时启用;
### xxl-job, access token
xxl.job.accessToken=ljwtoken

3.2. 打包部署“调度中心”

  • mvn clean package -Dmaven.test.skip=true

3.3. 访问调度中心

  • 调度中心访问地址:http://localhost:8080/xxl-job-admin ( 该地址执行器配置文件xxl.job.admin.addresses将会使用到,作为回调地址 )
  • 默认登录账号 “admin/123456”, 登录后运行界面如下图所示。
  • 调度中心集群部署参考

4. 配置部署 执行器项目

  • “执行器”项目:xxl-job-executor-sample-springboot (提供多种版本执行器供选择,现以 springboot 版本为例, 可直接使用,也可以参考其并将现有项目改造成执行器 )
  • 作用: 负责接收“调度中心”的调度并执行;可直接部署执行器,也可以将执行器集成到现有业务项目中。
  • 原理:
    • 执行器实际上是一个内嵌的Server,默认端口9999(配置项:xxl.job.executor.port)。
    • 在项目启动时,执行器会通过“@JobHandler”识别Spring容器中“Bean模式任务”,以注解的value属性为key管理起来。
    • “执行器”接收到“调度中心”的调度请求时,如果任务类型为“Bean模式”,将会匹配Spring容器中的“Bean模式任务”,然后调用其execute方法,执行任务逻辑。如果任务类型为“GLUE模式”,将会加载GLue代码,实例化Java对象,注入依赖的Spring服务(注意:Glue代码中注入的Spring服务,必须存在与该“执行器”项目的Spring容器中),然后调用execute方法,执行任务逻辑。

4.1 maven依赖

  • 执行器项目需要引入 “xxl-job-core” 的maven依赖,版本要和注册中心版本一致
<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>${project.parent.version}</version>
</dependency>

4.2 修改配置文件:

  • xxl-job-executor-sample-springboot/src/main/resources/application.properties
  • 修改调度中心部署跟地址
  • 需修改或自定义
    • xxl.job.admin.addresses 地址
    • xxl.job.executor.appname 自定义名称,后台配置必须对应
    • xxl.job.executor.ip 当前电脑Ip,或部署项目的电脑Ip
    • xxl.job.executor.port 端口
    • xxl.job.accessToken 自定义的token,要和admin模块配置的一致
### 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin

### xxl-job, access token 自定义的token,要和admin模块配置的一致
xxl.job.accessToken=ljwtoken

### xxl-job executor appname 自定义名称,后台配置必须对应
xxl.job.executor.appname=xxl-job-executor-sample
### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null
xxl.job.executor.address=
### xxl-job executor server-info
xxl.job.executor.ip=127.0.0.1
xxl.job.executor.port=9999

4.3 执行器组件配置 要配置执行器组件,配置文件参考地址: /xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java

@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
    logger.info(">>>>>>>>>>> xxl-job config init.");
    XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
    xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
    xxlJobSpringExecutor.setAppname(appname);
    xxlJobSpringExecutor.setAddress(address);
    xxlJobSpringExecutor.setIp(ip);
    xxlJobSpringExecutor.setPort(port);
    xxlJobSpringExecutor.setAccessToken(accessToken);
    xxlJobSpringExecutor.setLogPath(logPath);
    xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

    return xxlJobSpringExecutor;
}

4.4 部署执行器项目

  • 如果已经正确进行上述配置,将执行器项目编译打部署,我们使用springboot项目,打包运行xxl-job-executor-sample-springboot即可
  • mvn clean package -Dmaven.test.skip=true
  • 执行器项目集群部署参考

5. GLUE模式(Java)开发一个任务

  • GLUE模式(Java)原理:任务以源码方式维护在调度中心;该模式的任务实际上是一段继承自IJobHandler的Java类代码并 “groovy” 源码方式维护,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务(请确保Glue代码中的服务和类引用在“执行器”项目中存在),然后调用该对象的execute方法,执行任务逻辑。
  • GLUE模式(Java)就是在界面编写代码即可,不用在执行器项目中编写代码

5.1 新建任务

  • 登录调度中心,点击下图所示“新增”按钮,新建示例任务。然后,参考下面截图中任务的参数配置,点击保存。

image.png

5.2 “GLUE模式(Java)” 任务开发

  • 请点击任务右侧 “GLUE IDE” 按钮,进入 “GLUE编辑器开发界面” ,见下图。“GLUE模式(Java)” 运行模式的任务默认已经初始化了示例任务代码,即打印Hello World。

image.png

5.3 触发执行

  • 请点击任务右侧 “执行一次” 按钮,可手动触发一次任务执行(通常情况下,通过配置Cron表达式进行任务调度触发)。

5.4 查看日志:

  • 请点击任务右侧 “调度日志” 按钮,可前往任务日志界面查看任务日志。
  • 在任务日志界面中,可查看该任务的历史调度记录以及每一次调度的任务调度信息、执行参数和执行信息。运行中的任务点击右侧的“执行日志”按钮,可进入日志控制台查看实时执行日志。

6. BEAN模式开发一个任务

  • BEAN模式原理:任务以JobHandler方式维护在执行器端;需要结合 “JobHandler” ,任务类需要加“@JobHandler(value=“名称”)”注解,因为“执行器”会根据该注解识别Spring容器中的任务。任务类需要继承统一接口“IJobHandler”,任务逻辑在execute方法中开发,因为“执行器”在接收到调度中心的调度请求时,将会调用“IJobHandler”的execute方法,执行任务逻辑。

6.1 类形式

6.1.1 优缺点

  • Bean模式任务,支持基于类的开发方式,每个任务对应一个Java类。
    • 优点:
      • 不限制项目环境,兼容性好。即使是无框架项目,如main方法直接启动的项目也可以提供支持
    • 缺点:
      • 每个任务需要占用一个Java类,造成类的浪费;
      • 不支持自动扫描任务并注入到执行器容器,需要手动注入。

6.1.2 执行器项目中,开发Job类

  • 1、开发一个继承自"com.xxl.job.core.handler.IJobHandler"的JobHandler类,实现其中任务方法。
  • 2、手动通过如下方式注入到执行器容器。
    • :XxlJobExecutor.registJobHandler(“demoJobHandler”, new DemoJobHandler());

注: 版本v2.2.0 移除旧类注解@JobHandler,推荐使用基于方法注解@XxlJob的方式进行任务开发;(如需保留类注解JobHandler使用方式,可以参考旧版逻辑定制开发);

public class MyJobHandler extends IJobHandler {
    @Override
    public void execute() throws Exception {
        XxlJobHelper.log("11==============MyJobHandler=================");
        System.out.println("22==============MyJobHandler=================");
    }
}

注入myJobHandler:

@SpringBootApplication
public class XxlJobExecutorApplication {

    public static void main(String[] args) {
        SpringApplication.run(XxlJobExecutorApplication.class, args);

        XxlJobExecutor.registJobHandler("myJobHandler", new MyJobHandler());
    }

}

6.1.3 调度中心,新建调度任务

  • JobHandler填写@JobHandler的value值

image.png

  • 新建后可以测试运行一次查看日志是否成功

6.2 方法形式

6.2.1 优缺点

  • 基于方法开发的任务,底层会生成JobHandler代理,和基于类的方式一样,任务也会以JobHandler的形式存在于执行器任务容器中。
    • 优点:
      • 每个任务只需要开发一个方法,并添加@XxlJob注解即可,更加方便、快速。
      • 支持自动扫描任务并注入到执行器容器。
    • 缺点:要求Spring容器环境

6.2.2 执行器项目中,开发Job方法

    1. 任务开发:在Spring Bean实例中,开发Job方法
    1. 注解配置:为Job方法添加注解 “@XxlJob(value=“自定义jobhandler名称”, init = “JobHandler初始化方法”, destroy = “JobHandler销毁方法”)”,注解value值对应的是调度中心新建任务的JobHandler属性的值。
    1. 执行日志:需要通过 “XxlJobHelper.log” 打印执行日志
    1. 任务结果: 默认任务结果为 “成功” 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 “XxlJobHelper.handleFail/handleSuccess” 自主设置任务结果
@Configuration
public class MyXxlJobConfig {

    @XxlJob("myXxlJobConfig")
    public boolean demoJobHandler() throws Exception {
        XxlJobHelper.log("========myXxlJobConfig============");
        System.out.println("========myXxlJobConfig============");
        //ReturnT无作用
        //return new ReturnT(200, "hahahahah");
        return XxlJobHelper.handleSuccess("myXxlJobConfig hello world");
    }


    @XxlJob("myfaile")
    public boolean myfaile() throws Exception {
        XxlJobHelper.log("========myfaile============");
        System.out.println("========myfaile============");
        return XxlJobHelper.handleFail("myfaile hello world");
    }
    
 
    /**
     * 5、生命周期任务示例:任务初始化与销毁时,支持自定义相关逻辑;init只在第一次调度时执行一次。destroy每次调度都执行
     */
    @XxlJob(value = "demoJobHandler2", init = "init", destroy = "destroy")
    public void demoJobHandler2() throws Exception {
        System.out.println("========demoJobHandler2============");
        XxlJobHelper.log("XXL-JOB, Hello World.");
    }

    public void init() {
        System.out.println("========init============");
        logger.info("init");
    }

    public void destroy() {
        System.out.println("========destory============");
        logger.info("destory");
    }

}

6.2.3 调度中心,新建调度任务

  • 修改JobHandler为@XxlJob(“myfaile”)名称即可,其他照样新建任务就行

image.png

  • 新建后可以测试运行一次查看日志是否成功
  • myfaile方法返回为return XxlJobHelper.handleFail(“myfaile hello world”);是错误的,但是任务是调度成功的

image.png

注:

任务执行结果说明

系统根据以下标准判断任务执行结果

  • 任务执行成功:XxlJobHelper.handleSuccess()
  • 任务执行失败:XxlJobHelper.handleFail()
  • 注:任务调度室成功的,这里只是业务返回结果的判断

终止运行中的任务

仅针对执行中的任务。 在任务日志界面,点击右侧的“终止任务”按钮,将会向本次任务对应的执行器发送任务终止请求,将会终止掉本次任务,同时会清空掉整个任务执行队列。

输入图片说明

任务终止时通过 “interrupt” 执行线程的方式实现, 将会触发 “InterruptedException” 异常。因此如果JobHandler内部catch到了该异常并消化掉的话, 任务终止功能将不可用。

因此, 如果遇到上述任务终止不可用的情况, 需要在JobHandler中应该针对 “InterruptedException” 异常进行特殊处理 (向上抛出) , 正确逻辑如下:

try{
    // do something
} catch (Exception e) {
    if (e instanceof InterruptedException) {
        throw e;
    }
    logger.warn("{}", e);
}

而且,在JobHandler中开启子线程时,子线程也不可catch处理"InterruptedException",应该主动向上抛出。

任务终止时会执行对应JobHandler的"destroy()"方法,可以借助该方法处理一些资源回收的逻辑。

任务超时控制

  • 支持设置任务超时时间,任务运行超时的情况下,将会主动中断任务;
  • 需要注意的是,任务超时中断时与任务终止机制(可查看章节“终止运行中的任务”)类似,也是通过 “interrupt” 中断任务,因此业务代码需要将 “InterruptedException” 外抛,否则功能不可用。

最后

  • :+1:t2::有收获的,点赞鼓励!
  • :heart::收藏文章,方便回看!
  • :speech_balloon::评论交流,互相进步!

作者:小伙子vae
链接:一文搞懂⭐java中的定时任务框架-分布式(xxl-job) - 掘金