我是3y,一年CRUD
经验用十年的markdown
程序员👨🏻💻常年被誉为优质八股文选手
挺早就规划了要引入分布式定时任务框架了,在年前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
跟模板的增删改查耦合在一起了。
下一篇想来讲讲当定时任务被触发,得到了一个人群文件,我是怎么设计去调用消息进行推送下发的。
都看到这里了,点个赞一点都不过分吧?我是3y,下期见。
关注我的微信公众号【Java3y】除了技术我还会聊点日常,有些话只能悄悄说~ 【对线面试官+从零编写Java项目】 持续高强度更新中!求star!!原创不易!!求三连!!
austin项目源码Gitee链接:gitee.com/austin
austin项目源码GitHub链接:github.com/austin