背景
某日,领导找到我说:"阿雷啊,我们要在服务里加个定时任务,每天定时执行,你评估下要多长时间?"
我:"这个很简单啊,领导!用@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详解 保姆级教程含金量非常高,推荐大家去看看,我写这篇博客的目的更多的是记录和分享自己的实践过程,站在巨人的肩膀上,真的可以看得更高,更远。