Springboot整合XXL-JOB实现分布式定时作业调度

springboot整合XXL-JOB实现分布式定时作业调度

文档:

中文文档:https://www.xuxueli.com/xxl-job/

代码:https://github.com/xuxueli/xxl-job http://gitee.com/xuxueli0323/xxl-job

处理分片任务:https://blog.csdn.net/it_freshman/article/details/105421781

架构和原理剖析:https://mp.weixin.qq.com/s/vexhlS3nojWPj9ujVNNx4A

第一部分、XXL-JOB的功能,架构和原理

1、功能特性整理
  • 简单:支持通过Web页面对任务进行CRUD操作,操作简单,一分钟上手;
  • 动态:支持动态修改任务状态、启动/停止任务,以及终止运行中任务,即时生效;
  • 调度中心HA(中心式):调度采用中心式设计,“调度中心”自研调度组件并支持集群部署,可保证调度中心HA;
  • 执行器HA(分布式):任务分布式执行,任务”执行器”支持集群部署,可保证任务执行HA;
  • 注册中心:执行器会周期性自动注册任务,调度中心将会自动发现注册的任务并触发执行;
  • 弹性扩容缩容:一旦有新执行器机器上线或者下线,下次调度时将会重新分配任务;
  • 路由策略:执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等;
  • 故障转移:任务路由策略选择”故障转移”情况下,如果执行器集群中某一台机器故障,将会自动Failover切换到一台正常的执行器发送调度请求;
  • 阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度;
  • 任务超时控制:支持自定义任务超时时间,任务运行超时将会主动中断任务;
  • 任务失败重试:支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;其中分片任务支持分片粒度的失败重试;
  • 分片广播任务:执行器集群部署时,任务路由策略选择”分片广播”情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务,可根据分片参数开发分片任务;
  • 任务进度监控:支持实时监控任务进度;
  • Rolling实时日志:支持在线查看调度结果,并且支持以Rolling方式实时查看执行器输出的完整的执行日志;
  • 任务依赖:支持配置子任务依赖,当父任务执行结束且执行成功后将会主动触发一次子任务的执行,多个子任务用逗号分隔;
  • 触发方式:支持Cron方式、任务依赖方式、手动触发;
  • 一致性:“调度中心”通过DB锁保证集群分布式调度的一致性,一次任务调度只会触发一次执行;
  • 自定义任务参数:支持在线配置调度任务入参,即时生效;
  • 调度线程池:调度系统多线程触发调度运行,确保调度精确执行,不被堵塞;
  • 线程池隔离:调度线程池进行隔离拆分,慢任务自动降级进入”Slow”线程池,避免耗尽调度线程,提高系统稳定性;
  • 运行报表:支持实时查看运行数据,如任务数量、调度次数、执行器数量等;以及调度报表,如调度日期分布图,调度成功分布图等;
  • 全异步:任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰,理论上支持任意时长任务的运行。
可以扩展的功能主要有:
· 权限控制:接入转转SSO认证,自动识别当前登陆用户;统一以服务维度进行权限控制;
· 任务失败告警:扩展为企业微信的告警方式;
· 跨环境同步任务配置:在测试环境可一键将任务配置同步至沙箱、线上,避免业务反复创建任务;
· 可指定任务的执行实例列表:将任务限定在指定机器上执行,方便测试,同时避免实例代码不一致带来的干扰;
· 客户端封装成spring-boot-starter:starter自动区分当前环境和当前服务,并自动注册至调度平台,业务只需用@XxlJob和@JobHandler直接编写任务代码、创建和启动任务即可,免去注册服务等操作,真正实现一键接入,简单易用。
. 扩展服务对外接口,在应用程序中可以动态的创建定时任务
2、架构和分析
  • 调度中心:web服务,身兼Portal和调度中心的功能。主要负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除等,所有上述操作都会实时生效;同时支持监控调度结果以及执行日志、失败报警等,支持执行器Failover。

  • 执行器:部署在业务进程里。主要负责接收调度请求并执行任务的业务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效。

  • 其中,调度中心通过作者自研的xxl-rpc与执行器建立TCP长链接进行通信调度(注:2.2.0版本已改用语言无关的 RESTful API方式,方便跨语言对接调度中心或者实现执行器);外部组件仅依赖MySQL,多个调度中心进程也是通过MySQL锁来实现分布式锁的功能,避免重复调度。

  • 从整体来看,xxl-job架构依赖较少,功能强大,简约而不简单,方便部署,易于使用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EwgsVONy-1655701240342)(…/…/bak/md_img/image-20220424200011465.png)]

3、xxl-job的内部结构原理

(1)3.1 执行器的注册与发现

执行器的注册与发现通过以下两种方式:

· 主动式:执行器启动后,主动告知调度中心,并定时发送心跳;执行器要关闭时,也主动告知调度中心及时注销掉。

· 被动式:调度中心会有一个专门的后台线程定时调用所有注册上来的执行器接口,进行服务探活。如果发现执行器有异常会及时注销掉,避免任务再调度到该异常执行器上。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1do1sEc6-1655701240344)(…/…/bak/md_img/image-20220424202716743.png)]

3.2 任务的调度与执行

这块是xxl-job的核心部分。如上图,任务的整体调度与执行流程如下:

\1. 获取到MySQL锁的调度中心从MySQL获取未来5秒内的任务,并将任务加入到对应秒的时间轮槽里,然后更新任务的下次执行时间写入MySQL。例如当前时间是12:35,那么会加载12:40之前要运行的任务;若任务A要在12:37调度,那么会把A加入到37这一秒对应的时间轮槽里;

2.调度中心会有个专门线程每秒扫描时间轮,获取当前秒要执行的任务集合,并提交到线程池里。这里,线程池拆分为快、慢两个线程池,1分钟窗口期内任务耗时达500ms超过10次,该窗口期内判定为慢任务,慢任务自动降级进入”Slow”线程池,避免耗尽调度线程,提高系统稳定性;

3.线程池的工作线程向MySQL插入一条调度日志,并根据任务路由策略从服务的执行器列表中选择一个执行器(分片广播任务会选择所有执行器),然后将任务调度信息发送至目标执行器。调度信息主要包括调度日志ID、jobId、job名称、阻塞策略、超时时间、参数等;

4.执行器收到调度信息后,将调度信息加入到对应任务处理线程的等待队列中;任务处理程序串行从队列中取调度信息进行任务的执行,执行完后将结果写入一个公共的结果队列中。这里,每个任务有一个单独的处理线程和一个等待队列,调度信息放入队列,任务处理线程从队列里取调度信息进行执行,以此实现解耦;

5.执行器有一个专门的回调线程定时批量从结果队列里获取任务结果,并回调告知调度中心;

6.调度中心收到回调结果后,将结果更新至调度日志表中。如果任务失败,在此处可以进行重试。

由上可以看出,整个任务调度流程具如下特点:

1.秒级

2.快、慢线程池

3.全异步

4.优雅关闭:调度中心和执行器都有优雅关闭机制,避免任务丢失

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t9RwWWzG-1655701240350)(…/…/bak/md_img/image-20220424202753606.png)]

3.3 多调度中心如何互斥

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6WMAeI7j-1655701240353)(…/…/bak/md_img/image-20220424202823932.png)]

如上图所示,多调度中心通过MySQL锁来实现分布式锁进行互斥,从而避免任务的重复调度。MySQL里创建一个lock表,里边只有一行记录,lock_name字段值是“schedule_lock”。调度中心每次要加载任务时,需要开启一个手动事务,先获取schedule_task记录的写锁后,然后才能加载及更新任务;否则,说明其他调度中心已经获取成功在加载任务了,该调度中心只能等待其他调度中心事务提交才能再获取锁。流程的伪代码如下:

begin;
  select * from lockwhere lock_name = 'schedule_lock' for update;
  select * from jobwhere next_time < now()+5;
  put jobs intotimewheel;
  update jobs setnext_time=xxx where id in (...);
commit;
sleep 5s;
3.4 任务依赖

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KXMepb1G-1655701240355)(…/…/bak/md_img/image-20220424202910923.png)]

3.5 生命周期管理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EzVNIHjv-1655701240356)(…/…/bak/md_img/image-20220424202933574.png)]

3.6 执行日志

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6m1hOwOg-1655701240358)(…/…/bak/md_img/image-20220424202952149.png)]

4、XXL-JOB的扩展
权限控制:xxl-job自身的权限控制较弱,近乎没有。我们对其进行扩展,首先,我们接入转转SSO认证,并自动识别当前登陆用户信息;其次,给服务加上负责人信息,并将权限统一收归到服务的负责人。如果是服务的负责人,则有权限管理该服务下所有的任务。这些负责人是执行实例的starter在首次注册时,自动从CMDB(转转服务集群信息管理平台)来拉取的。

任务失败告警:转转员工目前统一使用企业微信,我们将任务失败告警也接入企业微信

跨环境同步任务配置:我们在测试、沙箱、线上共部署了三套任务调度平台。对于同一个任务,我们需要在这三个环境分别创建一次,用户体验不太友好。基于此,我们对其进行扩展,测试完成后,业务同学可在测试环境将任务一键同步至沙箱和测试环境,避免重复创建的繁琐

可指定任务的执行实例列表:xxl-job在任务调度时,根据路由策略从注册上来的执行器列表里选择一个来执行。但很多时候(比如线下并行测试),我们需要将任务固定在一个或者一批机器里来执行,方便定向观察。因此我们也对其进行扩展,业务同学可在任务里指定执行机器列表,调度时再根据路由策略从指定的机器列表里选择一个来执行

客户端封装成spring-boot-starter:业务同学在接入xxl-job时,需要手动注入XxlJobSpringExecutor Bean到Spring容器,还要进行一些基础配置,如调度中心地址、当前服务名称等。随后还要在任务调度平台手工注册服务,有一定的接入成本。为了简化接入流程,我们将这些配置统一化,将客户端封装成一个统一的、自动化的spring-boot-starter,它能自动区分当前环境和当前服务,自动注入Bean,并自动注册至对应环境的调度平台,业务只需用@XxlJob和@JobHandler直接编写任务代码、创建和启动任务即可,免去注册服务等操作,真正实现一键接入,简单易用。
5、总结
xxl-job是市面上为数不多的一款功能强大、简单实用的分布式任务调度平台,真正做到了开发迅速、学习简单、轻量级,其架构简约而不简单,方便部署、易于扩展。我们架构部引入xxl-job,并针对转转的业务场景,对其进行了二次扩展,方便转转同学快速接入、易于使用。

zzschedule从2020.02上线至今半年时间里,已有74个服务共276个执行实例,共计325个任务。总调度次数达到1900多万次,目前日均约12万次。因其功能强大、简单易用,大量业务线同学已从zzjob迁移至zzschedule,日益成为转转技术栈中不可或缺的一个重量级利器组件。

另外,zzschedule运行半年来,针对业务同学经常遇到的问题跟大家总结下,后续我们我们也尽量通过程序来规避这些问题:

· 任务启动前确认Cron表达式:经常发现有业务创建任务后直接启动任务就不管了,也没确认Cron表达式是否正确。比如上午9点调度一次的“0 0 9 * * ?”配置成了“* * 9 * * ?”,这就变成了上午9点这一小时内每秒都会调度一次,相差3600倍。所以新建任务或者修改Cron后,一定要在“下次执行时间”里确认下。

· 终止任务并不保证成功:终止调度中的任务是通过Thread.interrupt()来进行的。对于Java线程比较熟悉的同学应该知道,对一个线程interrupt,是否能够中断还要取决于线程的状态。如线程处于WAITTING状态,那么是响应中断的,抛出InterruptedException;但如果线程处于RUNNING状态,仅仅是将中断标志位置位。还有,WAITTING状态即便抛出InterruptedException,还要看业务代码有没有catch,如果被catch了,仍然终止不了。所以,这种终止机制是比较弱的,不保证一定能够终止。

· 调度日志不要狂打:如前面章节所讲,每次调度会生成一个日志文件,业务也可以通过XxlJobLogger.log来添加业务日志。这里建议大家添加业务日志时,只添加一些重要节点的日志,而不是在这里狂刷日志。非重要节点的业务日志,还是打到业务日志文件里为好。

第二部分:XXL-JOB的使用

1、添加依赖
<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>${project.parent.version}</version>
</dependency>
2、添加配置
  • admin-addresses 配置的是xxl服务的地址
  • appname 配置服务的应用名称
  • port 对应的端口
vipthink:
  icode:
    xxl:
      job:
        admin-addresses: http://icode-job.app.vipthink.net/xxl-job-admin/
        executor:
          appname: uat-vipthink-icode-course
          port: 9999
3、添加配置类
//=====================配置属性类=========================
@Data
@ConfigurationProperties("vipthink.icode.xxl.job")
public class XxlJobProperties {
   

    private String adminAddresses;

    private Executor executor;

    @Data
    public static class Executor {
   

        private int port;

        private String appName;

    }
}
//========================配置类=======================
@Slf4j
@Configuration
@EnableConfigurationProperties({
   XxlJobProperties.class})
public class XxlJobConfig {
   

    @Autowired
    private XxlJobProperties xxlJobProperties;

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
   
        log.info("xxl-job init");
        log.info("xxl-job config info {}", xxlJobProperties);
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(xxlJobProperties.getAdminAddresses());
        xxlJobSpringExecutor.setPort(xxlJobProperties.getExecutor().getPort());
        xxlJobSpringExecutor.setAppname(xxlJobProperties.getExecutor().getAppName());
        return xxlJobSpringExecutor;
    }
}
4、定时任务监听类
(1)定时任务分类
  • 简单任务,即直接执行,
  • 生命
  • 跨平台HTTP任务
  • 分片任务
  • 命令行任务
/**
 * XxlJob开发示例(Bean模式)
 *
 * 开发步骤:
 *      1、任务开发:在Spring Bean实例中,开发Job方法;
 *      2、注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。
 *      3、执行日志:需要通过 "XxlJobHelper.log" 打印执行日志;
 *      4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果;
 *
 * @author xuxueli 2019-12-11 21:52:51
 */
@Component
@Slf4j
public class SampleXxlJob {
   


    /**
     * 1、简单任务示例(Bean模式)
     *  任务执行完成默认成功
     */
    @XxlJob("demoJobHandler")
    public void demoJobHandler() throws Exception {
   
        XxlJobHelper.log("XXL-JOB, Hello World.");

        for (int i = 0; i < 5; i++) {
   
            XxlJobHelper.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值