XXL-JOB的基本用法

XXL-JOB是一个轻量级分布式任务调度框架,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。至今,XXL-JOB已接入多家互联网企业的线上产品线,接入场景如电商业务、O2O业务、金融业务和大数据作业等。

本文将通过一个SpringBoot示例工程,详述XXL-JOB的基本概念和用法。

一、环境描述

1. XXL-JOB服务器

2. Disconf服务器

3. Spring Tool Suite

4. Redis

二、使用XXL-JOB的原因

1. Quartz的不足

Quartz作为开源任务调度中的佼佼者,是任务调度的首选。但是在集群环境中,Quartz采用API的方式对任务进行管理,这样存在以下问题:

  • 通过调用API的方式操作任务,不人性化。
  • 需要持久化业务的QuartzJobBean到底层数据表中,系统侵入性相当严重。
  • 调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务。

XXL-JOB弥补了Quartz的上述不足之处。

2. RemoteHttpJobBean

常规的Quartz的开发,任务逻辑一般维护在QuartzJobBean中,耦合很严重。

XXL-JOB中“调度模块”和“任务模块”完全解耦,调度模块中的所有调度任务使用同一个QuartzJobBean,即RemoteHttpJobBean。不同的调度任务将各自的调度参数维护在各自的扩展表数据中,当触发RemoteHttpJobBean执行时,将会解析不同的调度参数发起远程调用,调用各自的远程执行器服务。

这种调用模型类似RPC调用,RemoteHttpJobBean提供调用代理的功能,而执行器提供远程服务的功能。

3. 架构设计

XXL-JOB将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”只负责发起调度请求。

将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中的业务逻辑。

因此,“调度”和“任务”两部分可以解耦成调度模块和执行模块,提高业务系统的整体稳定性和扩展性:

  • 调度模块(调度中心):负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统的性能不再受限于任务模块;支持可视化、简单且动态的管理调度信息,包括任务的新建、更新、删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。

  • 执行模块(执行器):负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;接收“调度中心”的执行请求、终止请求和日志请求等。

XXL-JOB的系统架构,如下图所示:

xxl-job的系统架构

三、建立SpringBoot示例工程

通过STS建立SpringBoot的示例工程,名称为xxl-job-demo。本文采用的配置如下:

  • SpringBoot版本为2.0.1
  • 项目依赖关系选择Web

这个示例工程将作为“执行器”,接收“调度中心”的调度请求,然后将Redis中的消息输出至任务日志。具体的创建步骤,本文不再赘述。

这个示例工程会用到Jedis客户端,其配置文件会交给Disconf托管,请参考《基于注解的分布式配置文件和配置项》,本文不再赘述。

四、整合XXL-JOB执行器

1. Maven依赖

打开pom.xml文件,添加XXL-JOB执行器的依赖关系,如下所示:

<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>1.9.1</version>
</dependency>

示例工程会自动从Maven仓库下载XXL-JOB执行器的jar包,如下图所示:

xxl-job执行器的依赖关系

2. 执行器配置文件

打开application.properties文件,添加执行器配置。application.properties文件的全部内容,如下所示:

# web port
server.port=8080

# log config
logging.config=classpath:logback.xml


### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://10.15.1.21:8081

### xxl-job executor address
xxl.job.executor.appname=xxl-job-demo
xxl.job.executor.ip=10.15.9.212
xxl.job.executor.port=9999

### xxl-job, access token
xxl.job.accessToken=

### xxl-job log path
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### xxl-job log retention days
xxl.job.executor.logretentiondays=-1

XXL-JOB执行器的相关配置项的意义,如下所示:

  • xxl.job.admin.addresses
    调度中心的部署地址。若调度中心采用集群部署,存在多个地址,则用逗号分隔。执行器将会使用该地址进行”执行器心跳注册”和”任务结果回调”。

  • xxl.job.executor.appname
    执行器的应用名称,它是执行器心跳注册的分组依据。

  • xxl.job.executor.ip
    执行器的IP地址,用于”调度中心请求并触发任务”和”执行器注册”。执行器IP默认为空,表示自动获取IP。多网卡时可手动设置指定IP,手动设置IP时将会绑定Host。

  • xxl.job.executor.port
    执行器的端口号,默认值为9999。单机部署多个执行器时,注意要配置不同的执行器端口。

  • xxl.job.accessToken
    执行器的通信令牌,非空时启用。

  • xxl.job.executor.logpath
    执行器输出的日志文件的存储路径,需要拥有该路径的读写权限。

  • xxl.job.executor.logretentiondays
    执行器日志文件的定期清理功能,指定日志保存天数,日志文件过期自动删除。限制至少保存3天,否则功能不生效。

注意,XXL-JOB执行器的配置文件也可以交给Disconf进行托管。

3. 执行器配置类

还需要新建一个执行器配置类,用来读取执行器的配置信息。新建一个名为com.example.demo.config的包,然后在这个包中新建一个名为XxlJobConfig的类,主要内容如下所示:

@Configuration
@ComponentScan(basePackages = "com.example.demo.jobhandler")
public class XxlJobConfig {
    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.executor.appname}")
    private String appName;

    @Value("${xxl.job.executor.ip}")
    private String ip;

    @Value("${xxl.job.executor.port}")
    private int port;

    @Value("${xxl.job.accessToken}")
    private String accessToken;

    @Value("${xxl.job.executor.logpath}")
    private String logPath;

    @Value("${xxl.job.executor.logretentiondays}")
    private int logRetentionDays;


    @Bean(initMethod = "start", destroyMethod = "destroy")
    public XxlJobExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobExecutor xxlJobExecutor = new XxlJobExecutor();
        xxlJobExecutor.setAdminAddresses(adminAddresses);
        xxlJobExecutor.setAppName(appName);
        xxlJobExecutor.setIp(ip);
        xxlJobExecutor.setPort(port);
        xxlJobExecutor.setAccessToken(accessToken);
        xxlJobExecutor.setLogPath(logPath);
        xxlJobExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobExecutor;
    }
}

XxlJobConfig配置类有两点需要注意:

  • 组件扫描
    第2行使用@ComponentScan注解,扫描com.example.demo.jobhandler包,将其中的任务处理器加载至Spring容器。

  • 获取执行器实例
    第29行的xxlJobExecutor()方法会实例化一个XXL-JOB执行器对象,执行器初始化时调用它的start()方法,执行器销毁时调用它的destroy()方法。

五、执行器

在浏览器中访问http://10.15.1.21:8081,登录XXL-JOB调度中心,默认的用户名/密码是admin/123456。

点击进入“执行器管理”页面,然后点击“新增执行器”按钮,弹出新增执行器窗口,如下图所示:

添加新的JobHandler

新增执行器时,需要填写的信息,如下所示:

  • AppName:这是用来唯一标识每个执行器集群的应用名称,执行器会周期性地以AppName为参数进行自动注册。可通过该配置自动发现注册成功的执行器,供任务调度时使用。

  • 名称:执行器的名称,因为AppName限制字母数字等组成,可读性不强,名称可以提高执行器的可读性。

  • 排序:执行器的排序,系统中需要执行器的地方,如任务新增,将会按照该排序读取可用的执行器列表。

  • 注册方式:调度中心获取执行器地址的方式,有以下两种:

    • 自动注册:执行器自动进行执行器注册,调度中心通过底层注册表可以动态发现执行器机器地址。
    • 手动录入:人工手动录入执行器的地址信息,多地址逗号分隔,供调度中心使用。
  • 机器地址:只有在“注册方式”为“手动录入”时可编辑,支持人工维护执行器的地址信息。

注意,AppName的取值应该和示例工程的application.properties文件中的xxl.job.executor.appname字段的取值相同,注册方式应该选择自动注册。新增完成之后,就可以在执行器列表中看到新建的执行器,如下图所示:

xxl-job的执行器列表

注意,执行器列表的“OnLine 机器地址”字段会在执行器启动时,显示执行器的IP地址和端口号。

六、任务

1. 任务调度属性

在XXL-JOB调度中心,点击进入“任务管理”页面,然后点击“新增任务”按钮,弹出新增任务窗口,如下图所示:

添加新的任务

新增任务时,需要填写的信息,如下所示:

  • 执行器:任务绑定的执行器,任务触发调度时将会自动发现注册成功的执行器,实现任务自动发现功能;另一方面,也可以方便地进行任务分组。每个任务必须绑定一个执行器,可以在“执行器管理”页面进行设置。

  • 任务描述:任务的描述信息,便于任务管理。

  • 路由策略:当执行器集群部署时,提供丰富的路由策略,包括:

    • FIRST(第一个):固定选择第一个机器。
    • LAST(最后一个):固定选择最后一个机器。
    • ROUND(轮询):轮流选择每台机器。
    • RANDOM(随机):随机选择在线的机器。
    • CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
    • LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举。
    • LEAST_RECENTLY_USED(最近最久未使用):最久为使用的机器优先被选举。
    • FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度。
    • BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度。
    • SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时传递分片参数;可根据分片参数开发分片任务。
  • Cron:触发任务执行的Cron表达式,请参考Cron的维基页面

  • 运行模式

    • BEAN模式:任务以JobHandler的方式维护在执行器端;需要结合 “JobHandler”属性匹配执行器中的任务;
    • GLUE模式(Java):任务以源码方式维护在调度中心;该模式的任务实际上是一段继承自IJobHandler的Java类代码并以“groovy”源码的方式维护,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务;
    • GLUE模式(Shell):任务以源码方式维护在调度中心;该模式的任务实际上是一段“shell”脚本;
    • GLUE模式(Python):任务以源码方式维护在调度中心;该模式的任务实际上是一段“python”脚本;
    • GLUE模式(NodeJS):任务以源码方式维护在调度中心;该模式的任务实际上是一段“nodejs”脚本;
  • JobHandler:只有在运行模式为“BEAN模式”时生效,对应执行器中新开发的JobHandler类的“@JobHandler”注解自定义的value值。

  • 子任务:每个任务都拥有一个唯一的任务ID(任务ID可以从任务列表获取),当本任务执行结束并且执行成功时,将会触发子任务ID所对应的任务的一次主动调度。

  • 阻塞处理策略:调度过于密集,执行器来不及处理时的处理策略:

    • 单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行。
    • 丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败。
    • 覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务。
  • 失败处理策略:调度失败时的处理策略:

    • 失败告警(默认):调度失败和执行失败时,都将会触发失败报警,默认会发送报警邮件。
    • 失败重试:调度失败时,除了进行失败告警之外,将会自动重试一次;注意在执行失败时不会重试,而是根据回调返回值判断是否重试。
  • 任务参数:任务执行所需的参数,多个参数时用逗号分隔,任务执行时将会把多个参数转换成数组传入。

  • 报警邮件:任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔。

  • 负责人:任务的负责人。

注意,编辑任务时也会弹出类似的窗口,其中的输入项请参考新增任务窗口。

接下来,本文将详述BEAN模式任务和GLUE(Java)模式任务,以及分片广播路由策略,这些是XXL-JOB最常用的功能。

2. BEAN模式

任务逻辑以JobHandler的形式存在于“执行器”所在项目中,开发流程如下:

Step-1 开发JobHandler代码

在示例工程中,新建com.example.demo.jobhandler包,用来存储任务的业务逻辑代码。在这个包中新建DemoJobHandler任务类,关键代码如下所示:

@JobHandler(value="demoJobHandler")
@Component
public class DemoJobHandler extends IJobHandler {

    @Autowired
    private RedisService redisService;

    @Override
    public ReturnT<String> execute(String param) throws Exception {
        String value = redisService.get("test_key", 1);
        XxlJobLogger.log(value);

        for (int i = 0; i < 5; i++) {
            XxlJobLogger.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        return SUCCESS;
    }
}

上述代码有三点需要注意:

  • 必须使用XXL-JOB的@JobHandler注解(第1行),指定JobHandler的名称为“demoJobHandler”,在调度中心新建任务的JobHandler字段的取值要与此相同。
  • 必须继承IJobHandler抽象类(第3行),并且实现它的execute()方法,这是实现任务逻辑的方法。
  • IJobHandler抽象类还有init()方法和destroy()方法,这两个方法是空方法,在任务实例初始化和销毁时调用,任务实现类可以选择性地覆盖这两个方法。
Step-2 新建调度任务

参考上文“任务调度属性”对新建的任务进行参数配置,运行模式选择“BEAN模式”,JobHandler属性填写任务注解@JobHandler中定义的值,如下图所示:

添加BEAN模式任务

调度中心会每隔15分钟调度一次demoJobHandler任务。

3. GLUE(Java)模式

任务以源码方式维护在调度中心,支持通过Web IDE在线更新,实时编译和生效,因此不需要指定JobHandler。开发流程如下:

Step-1 新建调度任务

参考上文“任务调度属性”对新建的任务进行参数配置,运行模式选择“GLUE模式(Java)”,如下图所示:

添加GLUE(Java)模式任务

调度中心会每隔15分钟调度一次这个任务。

Step-2 开发任务代码

在任务列表中选中指定的GLUE(Java)任务,点击该任务右侧的“GLUE”按钮,将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发(也可以在IDE中开发完成后,复制粘贴到编辑中)。

版本回溯功能:在GLUE任务的Web IDE界面,选择右上角下拉框“版本回溯”,会列出该GLUE任务的更新历史(支持30个版本的版本回溯),选择相应版本即可显示该版本代码,保存后GLUE代码即回退到对应的历史版本。GLUE任务代码和Web IDE界面,如下图所示:

Web IDE中的GLUE任务代码

4. 分片广播任务

执行器集群部署时,任务路由策略选择“分片广播”的情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时传递分片参数,可以根据分片参数开发分片任务。

“分片广播”以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。

“分片广播”和普通任务开发流程一致,不同之处在于可以获取分片参数,通过分片参数进行分片业务处理。开发流程如下:

Step-1 开发JobHandler代码

在示例工程的com.example.demo.jobhandler包中,新建ShardingJobHandler任务类,关键代码如下所示:

@JobHandler(value="shardingJobHandler")
@Service
public class ShardingJobHandler extends IJobHandler {

    @Override
    public ReturnT<String> execute(String param) throws Exception {

        // 分片参数
        ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo();
        XxlJobLogger.log("分片参数:当前分片序号 = {0}, 总分片数 = {1}", shardingVO.getIndex(), shardingVO.getTotal());

        // 业务逻辑
        for (int i = 0; i < shardingVO.getTotal(); i++) {
            if (i == shardingVO.getIndex()) {
                XxlJobLogger.log("第 {0} 片, 命中分片开始处理", i);
            } else {
                XxlJobLogger.log("第 {0} 片, 忽略", i);
            }
        }

        return SUCCESS;
    }
}

上述代码的第9行获取分片参数,第10行获取分片参数的两个属性:

  • shardingVO.getIndex()
    当前分片序号(从0开始),执行器集群列表中当前执行器的序号。

  • shardingVO.getTotal()
    总分片数,执行器集群的总机器数量。

Step-2 新建调度任务

参考上文“任务调度属性”对新建的任务进行参数配置,运行模式选择“BEAN模式”,路由策略选择“分片广播”,JobHandler属性填写任务注解@JobHandler中定义的值,如下图所示:

添加分片广播任务

调度中心会每隔15分钟广播调度一次shardingJobHandler任务。

分片广播的路由策略不仅适用于BEAN运行模式,而且也适用于GLUE(Java)运行模式。这项功能适用于以下业务场景:

  • 分片任务场景
    10个执行器的集群来处理10w条数据,每台机器只需要处理1w条数据,耗时降低10倍。
  • 广播任务场景
    广播执行器机器运行shell脚本、广播集群节点进行缓存更新等。

5. 任务列表

在XXL-JOB调度中心,点击进入“任务管理”页面,可以看到指定执行器的任务列表,如下图所示:

xxl-job的任务列表

在任务列表中,可以看到每个任务的任务ID、任务描述、运行模式、Cron、负责人和状态等信息。用户可以对任务进行以下几种操作:

  • 执行:手动触发一次任务调度,不影响原有调度规则。
  • 暂停/恢复:可对任务进行“暂停”和“恢复”操作。需要注意的是,此处的暂停/恢复仅针对任务的后续调度触发行为,不会影响到已经触发的调度任务。
  • 日志:可以查看任务历史调度日志。在历史调入日志界面可查看每次任务调度的调度结果、执行结果等,点击“执行日志”按钮可查看执行器完整日志。
  • 编辑:在弹出的“编辑任务”界面更新任务属性后保存即可,可以修改设置的任务属性信息。
  • GLUE:该操作仅针对GLUE任务。将会前往GLUE任务的Web IDE界面,在该界面支持对任务代码进行开发。
  • 删除:删除这个任务。

七、任务调度日志

在XXL-JOB调度中心,点击进入“调度日志”页面。

1. 查看调度日志

在“调度日志”页面可以查看每次任务调度的调度结果、执行结果等信息,如下图所示:

任务调度日志页面

从调度日志可以获取以下信息:

  • 调度时间:“调度中心”触发本次调度并向“执行器”发送任务执行信号的时间。
  • 调度结果:“调度中心”触发本次调度的结果,200表示成功,500或其他表示失败。
  • 调度备注:“调度中心”触发本次调度的日志信息。
  • 执行时间:“执行器”中本次任务执行结束后回调的时间。
  • 执行结果:“执行器”中本次任务执行的结果,200表示成功,500或其他表示失败。
  • 执行备注:“执行器”中本次任务执行的日志信息。

在示例工程中,调度日志位于/data/applogs/xxl-job/xxl-job-demo.log,可以在logback.xml文件中进行配置。

2. 查看执行日志

点击某行日志右侧的 “执行日志” 按钮,可跳转至执行日志界面,可以查看业务代码中打印的完整日志,如下图:

任务执行的详细日志

在示例工程中,执行日志位于/data/applogs/xxl-job/jobhandler目录中,可以在logback.xml文件中进行配置。

3. 终止运行中的任务

这项功能只针对执行中的任务。在任务日志页面,点击右侧的“终止任务”按钮,将会向本次任务对应的执行器发送任务终止请求,将会终止掉本次任务,同时会清空掉整个任务执行队列,如下图所示:

终止运行中的任务

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

因此, 如果遇到上述任务终止不起作用的情况, 需要在JobHandler中针对“InterruptedException”异常进行特殊处理(向上抛出)。另外,在JobHandler中开启子线程时,子线程也不可捕获处理“InterruptedException”,应该主动向上抛出。

4. 删除执行日志

在任务日志页面,选择执行器和任务之后,点击右侧的“清理”按钮将会出现“日志清理”弹框,弹框中支持选择不同类型的日志清理策略,选中后点击“确定”按钮即可进行日志清理操作,如下图所示:

清除日志窗口

八、结语

除了本文介绍的常用功能之外,XXL-JOB还有任务依赖、事件触发、访问令牌、故障转移等高级功能,本文由于篇幅限制就不一一介绍了。

通过示例工程可知,XXL-JOB具有相当强大的统一任务调度的功能,能够适用于绝大多数需要任务调度的业务场景。但是,金无足赤人无完人,XXL-JOB也有一个小缺点,那就是缺少用户权限机制,整个系统只有一个用户,它的用户名和密码固化在调度中心(服务器端)的xxl-job-admin.properties文件之中,如下图所示:

固化的用户名和密码

作者在XXL-JOB官方文档的TODO LIST中已经将任务权限管理功能提上日程,后续版本将会越来越完美!