Java如何实现定时任务?

挺早就规划了要引入分布式定时任务框架了,在年前austin就已经接入了,但代码过年一直都没写,文章也就一直拖到今天了。今天主要就跟大家在聊聊 定时任务 这个话题。

看完这篇文章你会了解到什么是定时任务,以及为什么austin项目要引入分布式定时任务框架,可以把代码下载下来看到我是怎么使用 xxl-job 的。

01、如何简单实现定时功能?

我是看视频入门Java的,那时候学Java基础API的时候,看的视频也带有讲定时功能(JDK原生就支持),我记得视频讲师写了 Timer 来讲解定时任务。

 

当时并不知道定时任务有什么实际作用,所以在初学阶段的我,从来没使用过Timer来实现定时的功能。

再后来,我学到 并发 了。那时候的讲师提到了 ScheduledExecutorService 这个接口,它比 Timer 更加强大,一般我们在JDK里可以用它来实现定时的功能

 

强就强在于 ScheduledExecutorService 内部是线程池, Timer 是单线程,它能更合理的利用资源。

我学并发的时候,我也并不太关注它(它并不是并发的重点),所以我也没用过 ScheduledExecutorService 来实现定时的功能。

后来吧,要到学习做项目了,那时候视频有个 Quartz 课程。我记得理解了很久,最后我才反应过来了,原来 写了这么多的代码 就是用它来实现定时的功能。

至于比 ScheduledExecutorService 和 Timer 好在哪里呢,最直观的是:它支持 cron 表达式。

 

为啥我会理解很久呢,因为 Quartz 的 api 太复杂了(它也有着自己的专业术语和概念性的东西)。不过这种跟着做项目的,我是一步一步跟着敲代码的。

而 Quartz 相关的API我是记不住了,但那时候我理解了:原来我们写代码可以靠「组件包」来完成想要的功能,原来这就是 cron 表达式。

等到我大三的时候,我想用自己学过的知识点来写个小项目,也算是梳理一遍自己到底学了什么东西。于是,我想起了 Quartz 。

那时候我已经学到了 Spring / SpringBoot 了。所以当我在网上搜 Spring 与 Quartz 整合的时候,了解到了 SpringTask ,再后来发现了 @Schedule 注解。

 

只需要一个简单的注解,就能实现定时任务的功能,并且支持 cron 表达式。

那那那那,还要个锤子的 Quartz 啊!

02、实习&&工作 定时任务

等我工作了之后,我学到了一个新的名词「 分布式定时任务框架 」。等我踏入职场了以后,我才发现原来定时任务这么好使!

列举下我真实工作时使用定时任务的常见姿势:

1、动态创建定时任务 推送 运营类的消息(定时推送消息)

2、广告结算定时任务 扫表 找到对应的可结算记录(定时扫表更新状态)

3、每天定时更新数据记录(定时更新数据)

还很多人问我有没有用过 分布式事务 ,我往往会回答:没有啊,我们都是 扫表 一把梭保证数据最终一致性的当然了,如果是面试的时候被问到,可以吹吹分布式事务。实际上是怎么扫表的呢?就是 定时 扫的咯。

另外,我当时简单看了下公司自研的分布式定时任务框架是怎么做的,我记得是基于 Quartz 进行扩展的,扩展有 failover 、 分片 等等机制。

一般来说,使用定时任务就是在 应用启动 或者 提前在Web页面 配置好定时任务(定时任务框架都是支持 cron 表达式的,所以是周期或者定时的任务),这种场景是最最最多的。

03、为什么分布式定时任务

在前面提到 Timer / ScheduledExecutorService / SpringTask(@Schedule) 都是单机的,但我们一旦上了生产环境,应用部署 往往 都是集群模式的。

在集群下,我们一般是希望 某个定时任务 只在某台机器上执行,那这时候,单机实现的定时任务就不太好处理了。

Quartz 是有 集群部署 方案的,所以有的人会利用 数据库行锁 或者使用 Redis分布式锁 来自己实现定时任务跑在某一台 应用机器 上;做肯定是能做的,包括有些挺出名的分布式定时任务框架也是这样做的,能解决问题。

但我们遇到的问题不单单只有这些,比如我想要支持 容错 功能(失败重试)、 分片 功能、 手动触发 一次任务、有一个比较好的管理定时任务的 后台界面 、 路由 负载均衡等等。这些功能,就是作为「 分布式定时任务框架 」所具备的。

既然现在已经有这么多的轮子了,那我们作为 使用方/需求方 就没必要自己重新实现一套了,用现有的就好了,我们可以学习现有轮子的实现设计思想。

04、分布式定时任务基础

Quartz 是优秀的开源组件,它将定时任务抽象了三个角色: 调度器 、 执行器 和 任务 ,以至于市面上的分布式定时任务框架都有类似角色划分。

对于我们使用方而言,一般是引入一个 client 包,然后根据它的规则(可能是使用注解标识,又或是实现某个接口),随后自定义我们自己的定时任务逻辑。

看着上面的执行图对应的角色抽象以及一般使用姿势,应该还是比较容易理解这个过程的。我们又可以再稍微思考两个问题:

1、 任务信息以及调度的信息是需要 存储 的,存储在哪?调度器是需要「 通知 」执行器去执行的,那「 通知 」是以什么方式去做?

2、调度器是怎么找到即将需要执行的任务的呢?

针对第一个问题,分布式定时任务框架又可以分成了 两个流派 :中心化和去中心化

  • 所谓的「中心化」指的是:调度器和执行器 分离 ,调度器统一进行调度,通知执行器去执行定时任务
  • 所谓的「去中心化」指的是:调度器和执行器 耦合 ,自己调度自己执行

对于「中心化」流派来说,存储相关的信息很可能是在 数据库 (DataBase),而我们引入的 client 包实际上就是 执行器 相关的代码。调度器 实现了任务调度 的逻辑, 远程调用 执行器触发对应的逻辑。

调度器「通知」执行器去执行任务时,可以是通过「RPC」调用,也可以是把任务信息写入消息队列给执行器消费来达到目的。

对于「去中心化」流派来说存储相关的信息很可能是在 注册中心 (Zookeeper),而我们引入的 client 包实际上就是 执行器+调度器 相关的代码。

依赖注册中心来完成 任务的分配 ,「中心化」流派在调度的时候是需要保证一个任务只被一台机器消费,这就需要在代码里写分布式锁相关逻辑进行保证,而「去中心化」依赖注册中心就免去了这个环节。

针对第二个问题,调度器是怎么找到即将需要执行的任务的呢?现在一般较新的分布式定时任务框架都用了「 时间轮 」。

1、如果我们日常要找到准备要执行的任务,可能会把这些任务放在一个 List 里然后进行判断,那此时查询的时间复杂度为O(n)

2、稍微改进下,我们可能把这些任务放在一个最小堆里(对时间进行排序),那此时的增删改时间复杂度为O(logn),而查询是O(1)

3、再改进下,我们把这些任务放在一个 环形数组 里,那这时候的增删改查时间复杂度都是O(1)。但此时的环形数组大小决定着我们能存放任务的大小,超出环形数组的任务就需要用另外的数组结构存放。

4、最后再改进下,我们可以有 多层 环形数组,不同层次的环形数组的 精度 是不一样的,使用多层环形数组能大大提高我们的精度。

05、分布式定时任务框架选型

分布式定时任务框架现在可选择的还是挺多的,比较出名的有: XXL-JOB / Elastic-Job / LTS / SchedulerX / Saturn / PowerJob 等等等。有条件的公司可能会基于 Quartz 进行拓展,自研一套符合自己的公司内的分布式定时任务框架。

我并不是做这块出身的,对于我而言,我的 austin 项目技术选型主要会关注两块(其实跟选择apollo作为分布式配置中心的理由是一样的): 成熟、稳定、社区是否活跃 。

这一次我选择了 xxl-job 作为 austin 的分布式任务调度框架。 xxl-job 已经有很多公司都已经接入了(说明他的 开箱即用 还是很到位的)。不过最新的一个版本在 2021-02 ,近一年没有比较大的更新了。

06、为什么austin需要分布式定时任务框架

回到 austin 的系统架构上, austin-admin 后台管理页面已经被我造出来了,这个后台管理系统会提供「消息模板」的管理功能。

那发送一条消息不单单是「技术侧」调用接口进行发送的,还有很多是「运营侧」通过设置定时进而推送。

而这个功能,就需要用到分布式定时任务框架作为中间件支撑我的业务,并且很重要的一点:分布式定时任务框架需要支持 动态 创建定时任务的功能。

当在页面点击「 启动 」的时候,就需要创建一个定时任务,当在页面点击「 暂停 」的时候,就需要停止定时任务,当在页面点击「 删除 」模板的时候,如果曾经有过定时任务,就需要把它给一起删掉。当在页面点击「编辑」并保存的时候,也需要把停止定时任务。

嗯,所需要的流程就这些了

07、austin接入xxl-job

接入 xxl-job 分布式定时任务框架的步骤还是蛮简单的(看下文档基本就会了),我简单说下吧。接入具体的代码大家可以拉 ausitn 的下来看看,我会重点讲讲我接入时的感受。

官网文档: https://www.xuxueli.com/xxl-job/#%E4%BA%8C%E3%80%81%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8

1、自己项目上引入 xxl-job-core 的maven依赖

2、在MySQL中执行 /xxl-job/doc/db/tables_xxl_job.sql 的SQL脚本

3、从 Gitee 或 GitHub 下载 xxl-job 的源码,修改 xxl-job-admin 调度中心的 数据库 配置,启动 xxl-job-admin 项目。

4、在自己项目上添加 xxl-job 相关的配置信息

5、使用 @XxlJob 注解修饰方法编写定时任务的相关逻辑

从接入或者已经看过文档的小伙伴应该就很容易发现, xxl-job 它是属于「中心化」流派的分布式定时任务框架,调度器和执行器是分离的。

在前面我提到了 austin 需要 动态 增删改定时任务,而 xxl-job 是支持的,但我觉得没封装得足够好,只在调度器上给出了 http 接口。而调用 http 接口是相对麻烦的,很多相关的 JavaBean 都没有在 core 包定义,只能我自己再写一次。

所以,我花了挺长的时间和挺多的代码去完成 动态 增删改定时任务这个工作。

调度器和执行器是分开部署的,意味着,调度器和执行器的网络是必须 可通 的:原本我在本地是没有装任何的环境的,包括MySQL我都是连接云服务器的,但是现在我要调试就必须在网络可通的环境内,所以我不得不在本地启动 xxl-job-admin 调度中心来调试。

在启动执行器的时候,会开一个新的端口给 xxl-job-admin 调度中心调用而不是复用SpringBoot默认端口也是挺奇怪的?

08、总结

这篇文章主要讲了什么是定时任务、为什么要用定时任务、在Java领域中如果有定时任务相关的需求可以用什么来实现、分布式定时任务的基础知识以及如何接入 XXL-JOB

相信大家对分布式定时任务框架有了个基本的了解,如果感兴趣可以挑个开源框架去学学,想了解接入的代码可以把我的 austin 项目拉下来看看。

主要的代码就在 austin-cron 的 xxl 包下,而分布式应用的代码主要在 austin-web 的 MessageTemplateController 跟模板的增删改查耦合在一起了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值