你还不知道XXL-JOB?

背景

某日,领导找到我说:"阿雷啊,我们要在服务里加个定时任务,每天定时执行,你评估下要多长时间?"

我:"这个很简单啊,领导!用@Scheduled注解就能实现!"

java 复制代码
/**
 *
 * @author leixiyueqi
 * @since 2024/7/3 22:00
 */
@EnableScheduling  // 启动类要加这个注解
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

/**
 * 测试服务类
 */
@Component
public class DemoJob {
    // 测试任务,每五秒执行一次
    @Scheduled(cron = "0/5 * * * * ?")
    public void demoJob(){
        System.out.println("demoJob");
    }
}

我三下五除二的整理好代码,发给他:"领导,您看我这效率咋样?"

领导瞟了一眼,说:"效率咋样我不知道,但你这代码肯定是不行的,咱这是集群架构,你这代码部署上去,多个服务同时跑,不就做重复工作了吗?"

我:"那......我再加个分布式锁?"

见我按住了CTRL键,领导一把拉住我:"你先别忙着抄,客户还有要求呢,你听我说完!"

他清了清嗓子:"这个任务不仅可以定时触发,还要支持手动触发!"

我:"那我在服务里加个Controller,开放一个API接口?"

领导:"对于多套服务集群的场景,不仅支持指定服务执行,还能支持轮询,随机等多种路由策略在服务器中调度任务。"

我:"那......我得写个服务,专门调这个请求,在服务里集成这些算法。"

领导:"在任务执行不成功时,得支持重发,还要给运维人员发邮件!"

我:"那......服务里加个容错机制,附带邮件发送服务?"

领导:"每次执行都要记录日志,而且客户希望能查询执行日志,定位错误原因!"

我:"客户咋那么多事呢?"

领导(语气加重):"嗯?"

我(冷汗):"客户的需求好细致!"

领导:"定时任务的执行频率,执行时间,客户希望能自己随意配置!"

我:"啊?"

领导:"定时任务启不启用,什么时候启用也不确定,你得做个开关,客户想用时点一下就能启动任务,不想用了可以再关掉。

我:"还......还要这样?"

领导:"其他的需求我就不说了,国际化,线程池调度,分片策略,多语言支持......说了你也做不出来!给你两个小时,你先把这些东西做出来!"

我:"两,两个小时?领导您想为难我就直说啊,就这些功能,您给我一个月都做不出来!"

领导一副恨铁不成钢的样子,语重心长的说:"阿雷啊,你知道人和动物最大的区别是什么吗?"

"什么?"

"人会使用工具啊!"

"......"

"说了这么多,难道你就没想过用现成的开源组件吗?比如......XXL-JOB?"

XXL-JOB介绍

XXL-JOB是一个分布式任务调度平台,可以实现如上所述的所有功能,集任务定时调度,任务管理,统计,故障报警,集群等多种优点于一身。其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。至于它的优点以及源码,大家可以在xxl-job源码及功能介绍中查看,在这里就不多赘述了。

XXL-JOB实践

1、下载源码包,在上文提到的链接中下载。

下图是下载下来的源码的结构:

2、本地初始化数据库,XXL-JOB默认支持mysql数据库,可以将源码包中的tables_xxl_job.sql在本地的mysql服务器中执行。

3、修改数据库连接配置,在xxl-job-admin\src\main\resources\application.properties中修改数据库的配置。

4、启动xxl-job,访问地址http://localhost:8080/xxl-job-admin 初始登陆用户名/密码为 admin/123456

5、修改原服务中的定时任务,修改内容如下:

java 复制代码
<!--pom.xml中添加maven依赖,注意这个版本最好与xxl-job-admin中的版本一致-->

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


/**
 * xxl-job 添加xxl-job 的config 文件
 *
 * @author xuxueli 2017-04-28
 */
@Configuration
public class XxlJobConfig {
    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

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

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

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

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

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

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

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

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


    @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;
    }
}

// 修改原来的job类。
@Component
public class DemoJob {

    // 测试任务,每五秒执行一次
    //@Scheduled(cron = "0/5 * * * * ?")
    @XxlJob("demoJob")
    public void demoJob(){
        System.out.println("demoJob");
    }
}

// yml 配置添加

xxl:
  job:
    enabled: true
    admin:
      addresses: http://127.0.0.1:8080/xxl-job-admin
    # xxl-job ,access token
    accessToken: default_token
    executor:
      # 执行器名称
      appname: scheduled-xxl-job
      # 执行器地址
      address:
      # 执行器注册IP 自动注册可不填写
      ip:
      # 执行器注册端口
      port: 80
      # 执行日志保存路径
      logpath: ./logs/jobhandler
      # 执行日志保存时间
      logretentiondays: 30

6、XXL-JOB中添加执行器,这一步相当于把服务在xxl-job-admin中注册。

7、XXL-JOB中配置定时任务,Cron里集成了各种定时策略,JobHandler里要写调用的任务名,即客户端里@XxJob里的"demoJob"。

7、启动定时任务,查看本地的日志,与期望中的结果一致。以前通过@Scheduled方式执行的任务变成了通过XXL-JOB来调用。

源码分析

在完成了上述测试后,我忽然萌生了一个想法:XXL-JOB的功能是怎么实现的?真的是我设想的那样,创建了一个微服务,定时去调其他的服务吗?我决定好好看看xxl-job的源码,以下是我的收获。

一、客户端方法注册

这里的客户端指的是被调用方(即方法的实现位置),通过上面的实践我们知道,在客户端服务类的方法上添加一个@XxlJob注解,就可以让服务方(xxl-job-admin)通过注解名来调用该方法。

要实现这样的效果,客户端在启动时首先要知道哪些方法可以被调用,通过分析源码得知,它是通过XxlJobSpringExecutor 中的initJobHandlerMethodRepository方法,找到所有配置了该注解的服务方法,将这些方法以Map<方法名,IJobHandler>的形式存储到jobHandlerRepository中。

XxlJobSpringExecutor实现了SmartInitializingSingleton接口,它是Spring框架中的一个接口,用于在单例bean初始化完成后执行自定义逻辑,它提供了更灵活的初始化机制。当其他类实现了SmartInitializingSingleton接口并被Spring容器管理时,容器会在所有单例bean初始化完成后调用afterSingletonsInstantiated方法。这个方法可以用来执行一些需要在所有单例bean初始化完成后进行的逻辑操作,例如这里的方法注册。

二、客户端方法被调用

通过Debug发现,客户端的方法是在EmbedServer里的成员类EmbedHttpServerHandler里的方法channelRead0中被调用的。方法的调用链为channelRead0() -> process() -> ExecutorBizImpl.run() ->XxlJobExecutor.registJobThread(),而在ExecutorBizImpl.run()方法中,就取了上文中说到的jobHandlerRepository中的IJobHandler对象,通过registJobThread()在线程中执行方法。

但是,我在服务中全文搜索后,发现根本没地方调用过channelRead0()方法,仔细检查后发现,该方法是EmbedServer成员类EmbedHttpServerHandler继承SimpleChannelInboundHandler类后重写的方法,而SimpleChannelInboundHandler中的方法channelRead()会调用channelRead0()。

SimpleChannelInboundHandler 继承的ChannelInboundHandlerAdapter又是Netty框架中的一个类,用于处理入站事件的处理器适配器。它实现了ChannelInboundHandler接口,提供了一些默认的实现方法,方便开发者自定义处理入站事件的逻辑。当Netty的Channel接收到入站事件时,ChannelInboundHandlerAdapter将会被调用来处理这些事件。以下是该类的一些常用方法:

1、channelRegistered(ChannelHandlerContext ctx): 当Channel注册到EventLoop时调用。

2、channelUnregistered(ChannelHandlerContext ctx): 当Channel从EventLoop取消注册时调用。

3、channelActive(ChannelHandlerContext ctx): 当Channel处于活动状态时调用,表示已经与远程对等方建立了连接。

4、channelInactive(ChannelHandlerContext ctx): 当Channel不再处于活动状态时调用,表示已经与远程对等方断开连接。

5、channelRead(ChannelHandlerContext ctx, Object msg): 当Channel接收到数据时调用,可以在这里处理接收到的数据。

6、channelReadComplete(ChannelHandlerContext ctx): 在数据读取完成后调用,可以在这里执行一些清理操作或发送响应数据。

7、exceptionCaught(ChannelHandlerContext ctx, Throwable cause): 当出现异常时调用,可以在这里处理异常情况。

到了这里才明白,原来XXL-JOB的客户端底层居然是通过Netty实现的,那么,服务端又是怎么调用客户端的呢?咱们接着往下看。

三、服务端调用方法

通过分析源码得知,服务端的JobScheduleHelper类会无限循环的查询5秒内要做的任务,然后通过线程执行任务。

下层的方法是JobTriggerPoolHelper.addTrigger(),再下层的XxlJobRemotingUtil.postBody()方法相信大家就不陌生了,就是一个http请求

至此,我完成的XXL-JOB从服务端到客户端的一个实现逻辑的剖析,虽然什么都没做,但我还是感觉好有成就感的样子。从设计实现上来看,这个XXL-JOB和我最初的设想也大差不差嘛,只不过人家把想法付诸实现了而己(可把我给牛批坏了......)

综上所述,本质上定时任务的实现是通过XXL-JOB服务端定时的调用客户端的服务来实现的,所以服务端和客户端一定要保证网络上的互通。很多生产环境中如果存在防火墙或网络限制,导致XXL-JOB服务端无法访问客户端,就会导致定时任务调度失败,这是需要注意的。

致谢

吃水不忘挖井人,非常感谢大神许雪里研发并开源的XXL-JOB,为广大同仁们提供了便利,其实在实践XXL-JOB前,我也在网上参考了很多资料,其中这位大神的XXL-JOB详解 保姆级教程含金量非常高,推荐大家去看看,我写这篇博客的目的更多的是记录和分享自己的实践过程,站在巨人的肩膀上,真的可以看得更高,更远。

相关推荐
柏油15 分钟前
Spring @TransactionalEventListener 解读
spring boot·后端·spring
小小工匠1 小时前
Maven - Spring Boot 项目打包本地 jar 的 3 种方法
spring boot·maven·jar·system scope
板板正3 小时前
Spring Boot 整合MongoDB
spring boot·后端·mongodb
泉城老铁4 小时前
在高并发场景下,如何优化线程池参数配置
spring boot·后端·架构
泉城老铁4 小时前
Spring Boot中实现多线程6种方式,提高架构性能
spring boot·后端·spring cloud
hrrrrb5 小时前
【Java Web 快速入门】九、事务管理
java·spring boot·后端
布朗克1687 小时前
Spring Boot项目通过RestTemplate调用三方接口详细教程
java·spring boot·后端·resttemplate
IT毕设实战小研8 小时前
基于Spring Boot校园二手交易平台系统设计与实现 二手交易系统 交易平台小程序
java·数据库·vue.js·spring boot·后端·小程序·课程设计
孤狼程序员8 小时前
【Spring Cloud 微服务】1.Hystrix断路器
java·spring boot·spring·微服务
RainbowSea8 小时前
伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 04
java·spring boot·后端