我是3y,一年CRUD经验用十年的markdown程序员👨🏻‍💻常年被誉为优质八股文选手

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

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

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

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

Java如何实现定时任务?-LMLPHP

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

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

Java如何实现定时任务?-LMLPHP

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

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

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

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

Java如何实现定时任务?-LMLPHP

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

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

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

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

Java如何实现定时任务?-LMLPHP

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

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

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

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

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

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

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

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

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

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

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

Java如何实现定时任务?-LMLPHP

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

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

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

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

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

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

04、分布式定时任务基础

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

Java如何实现定时任务?-LMLPHP

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

Java如何实现定时任务?-LMLPHP

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

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

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

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

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

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

Java如何实现定时任务?-LMLPHP

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

Java如何实现定时任务?-LMLPHP

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

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

Java如何实现定时任务?-LMLPHP

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

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

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

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

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

Java如何实现定时任务?-LMLPHP

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

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

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

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

Java如何实现定时任务?-LMLPHP

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

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

Java如何实现定时任务?-LMLPHP

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

Java如何实现定时任务?-LMLPHP

Java如何实现定时任务?-LMLPHP

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

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

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

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、从GiteeGitHub下载xxl-job的源码,修改xxl-job-admin调度中心的数据库配置,启动xxl-job-admin项目。

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

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

Java如何实现定时任务?-LMLPHP

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

Java如何实现定时任务?-LMLPHP

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

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

Java如何实现定时任务?-LMLPHP

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

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

Java如何实现定时任务?-LMLPHP

08、总结

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

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

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

下一篇想来讲讲当定时任务被触发,得到了一个人群文件,我是怎么设计去调用消息进行推送下发的。

都看到这里了,点个赞一点都不过分吧?我是3y,下期见。

Java如何实现定时任务?-LMLPHP

关注我的微信公众号【Java3y】除了技术我还会聊点日常,有些话只能悄悄说~ 【对线面试官+从零编写Java项目】 持续高强度更新中!求star!!原创不易!!求三连!!

Java如何实现定时任务?-LMLPHP

austin项目源码Gitee链接:gitee.com/austin

austin项目源码GitHub链接:github.com/austin

更多的文章可往:文章的目录导航
03-25 09:07