你还不知道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详解 保姆级教程含金量非常高,推荐大家去看看,我写这篇博客的目的更多的是记录和分享自己的实践过程,站在巨人的肩膀上,真的可以看得更高,更远。

相关推荐
AskHarries2 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion3 小时前
Springboot的创建方式
java·spring boot·后端
Yvemil73 小时前
《开启微服务之旅:Spring Boot Web开发举例》(一)
前端·spring boot·微服务
星河梦瑾4 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
计算机学长felix6 小时前
基于SpringBoot的“交流互动系统”的设计与实现(源码+数据库+文档+PPT)
spring boot·毕业设计
.生产的驴6 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲6 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
撒呼呼6 小时前
# 起步专用 - 哔哩哔哩全模块超还原设计!(内含接口文档、数据库设计)
数据库·spring boot·spring·mvc·springboot
因我你好久不见6 小时前
springboot java ffmpeg 视频压缩、提取视频帧图片、获取视频分辨率
java·spring boot·ffmpeg
Yvemil77 小时前
《开启微服务之旅:Spring Boot Web开发》(二)
前端·spring boot·微服务