最近有朋友问到定时任务相关的问题。
于是,我简单写了一篇文章总结一下定时任务的一些概念以及一些常见的定时任务技术选型。希望能对小伙伴们有帮助!
为什么需要定时任务?我们来看一下几个非常常见的业务场景:
某系统凌晨要进行数据备份。
某电商平台,用户下单半个小时未支付的情况下需要自动取消订单。
某媒体聚合平台,每10分钟动态抓取某某网站的数据为自己所用。
某博客平台,支持定时发送文章。
某基金平台,每晚定时计算用户当日收益情况并推送给用户最新的数据。
这些场景往往都要求我们在某个特定的时间去做某个事情。
单机定时任务技术选型Timer是开始就已经支持的一种定时任务的实现方式。
Timer内部使用一个叫做TaskQueue的类存放定时任务,它是一个基于最小堆实现的优先级队列。TaskQueue会按照任务距离下一次执行时间的大小将任务排序,保证在堆顶的任务最先执行。这样在需要执行任务时,每次只需要取出堆顶的任务运行即可!
Timer使用起来比较简单,通过下面的方式我们就能创建一个1s之后执行的定时任务。
//示例代码:TimerTasktask=newTimerTask(){publicvoidrun(){("当前时间:"+newDate()+"n"+"线程名称:"+().getName());}};("当前时间:"+newDate()+"n"+"线程名称:"+().getName());Timertimer=newTimer("Timer");longdelay=1000L;(task,delay);//输出:当前时间:FriMay2815:18:47CST2025n线程名称:main当前时间:FriMay2815:18:48CST2025n线程名称:Timer不过其缺陷较多,比如一个Timer一个线程,这就导致Timer的任务的执行只能串行执行,一个任务执行时间过长的话会影响其他任务(性能非常差),再比如发生异常时任务直接停止(Timer只捕获了InterruptedException)。
Timer类上的有一段注释是这样写的:
*Thisclassdoesnotofferreal-timeguarantees:itschedules*(long)/ttmethod.*{@}packageand*oneoftheconcurrencyutilitiesthereinisthe{@link**ScheduledThreadPoolExecutor}whichisathreadpoolforrepeatedly**versatilereplacementforthe{@codeTimer}/{@codeTimerTask}*combination,asitallowsmultipleservicethreads,acceptsvarious*timeunits,anddoesn'trequiresubclassing{@codeTimerTask}(just*implement{@codeRunnable}).Configuring{@code*ScheduledThreadPoolExecutor}withonethreadmakesitequivalentto*{@codeTimer}.大概的意思就是:ScheduledThreadPoolExecutor支持多线程执行定时任务并且功能更强大,是Timer的替代品。
ScheduledExecutorServiceScheduledExecutorService是一个接口,有多个实现类,比较常用的是ScheduledThreadPoolExecutor。
ScheduledThreadPoolExecutor本身就是一个线程池,支持任务并发执行。并且,其内部使用DelayQueue作为任务队列。
//示例代码:TimerTaskrepeatedTask=newTimerTask(){@SneakyThrowspublicvoidrun(){("当前时间:"+newDate()+"n"+"线程名称:"+().getName());}};("当前时间:"+newDate()+"n"+"线程名称:"+().getName());ScheduledExecutorServiceexecutor=(3);longdelay=1000L;longperiod=1000L;(repeatedTask,delay,period,);(delay+period*5);();//输出:当前时间:FriMay2815:40:46CST2025n线程名称:main当前时间:FriMay2815:40:47CST2025n线程名称:pool-1-thread-1当前时间:FriMay2815:40:48CST2025n线程名称:pool-1-thread-1当前时间:FriMay2815:40:49CST2025n线程名称:pool-1-thread-2当前时间:FriMay2815:40:50CST2025n线程名称:pool-1-thread-2当前时间:FriMay2815:40:51CST2025n线程名称:pool-1-thread-2当前时间:FriMay2815:40:52CST2025n线程名称:pool-1-thread-2不论是使用Timer还是ScheduledExecutorService都无法使用Cron表达式指定任务执行的具体时间。
SpringTask我们直接通过Spring提供的@Scheduled注解即可定义定时任务,非常方便!
/***cron:使用Cron表达式。每分钟的1,2秒运行*/@Scheduled(cron="1-2****?")publicvoidreportCurrentTimeWithCronExpression(){("CronExpression:Thetimeisnow{}",(newDate()));}我在大学那会做的一个SSM的企业级项目,就是用的SpringTask来做的定时任务。
但是,Spring自带的定时调度只支持单机,并且提供的功能比较单一。之前写过一篇文章:《5分钟搞懂如何在SpringBoot中ScheduleTasks》,不了解的小伙伴可以参考一下。
SpringTask底层是基于JDK的ScheduledThreadPoolExecutor线程池来实现的。
优缺点总结:
优点:简单,轻量,支持Cron表达式
缺点:功能单一
时间轮Kafka、Dubbo、ZooKeeper、Netty、Caffeine、Akka中都有对时间轮的实现。
时间轮简单来说就是一个环形的队列(底层一般基于数组实现),队列中的每一个元素(时间格)都可以存放一个定时任务列表。
时间轮中的每个时间格代表了时间轮的基本时间跨度或者说时间精度,加入时间一秒走一个时间格的话,那么这个时间轮的最高精度就是1秒(也就是说3s和3.9s会在同一个时间格中)。
下图是一个有12个时间格的时间轮,转完一圈需要12s。当我们需要新建一个3s后执行的定时任务,只需要将定时任务放在下标为3的时间格中即可。当我们需要新建一个9s后执行的定时任务,只需要将定时任务放在下标为9的时间格中即可。
那当我们需要创建一个13s后执行的定时任务怎么办呢?这个时候可以引入一叫做圈数/轮数的概念,也就是说这个任务还是放在下标为3的时间格中,不过它的圈数为2。
除了增加圈数这种方法之外,还有一种多层次时间轮(类似手表),Kafka采用的就是这种方案。
针对下图的时间轮,我来举一个例子便于大家理解。
上图的时间轮,第1层的时间精度为1,第2层的时间精度为20,第3层的时间精度为400。假如我们需要添加一个350s后执行的任务A的话(当前时间是0s),这个任务会被放在第2层(因为第二层的时间跨度为20*20=400350)的第350/20=17个时间格子。
当第一层转了17圈之后,时间过去了340s,第2层的指针此时来到第17个时间格子。此时,第2层第17个格子的任务会被移动到第1层。
任务A当前是10s之后执行,因此它会被移动到第1层的第10个时间格子。
这里在层与层之间的移动也叫做时间轮的升降级。参考手表来理解就好!
时间轮比较适合任务数量比较多的定时任务场景,它的任务写入和执行的时间复杂度都是0(1)。
分布式定时任务技术选型上面提到的一些定时任务的解决方案都是在单机下执行的,适用于比较简单的定时任务场景比如每天凌晨备份一次数据。
如果我们需要一些高级特性比如支持任务在分布式场景下的分片和高可用的话,我们就需要用到分布式任务调度框架了。
通常情况下,一个定时任务的执行往往涉及到下面这些角色:
任务:首先肯定是要执行的任务,这个任务就是具体的业务逻辑比如定时发送文章。
调度器:其次是调度中心,调度中心主要负责任务管理,会分配任务给执行器。
执行器:最后就是执行器,执行器接收调度器分派的任务并执行。
Quartz一个很火的开源任务调度框架,完全由Java写成。Quartz可以说是Java定时任务领域的老大哥或者说参考标准,其他的任务调度框架基本都是基于Quartz开发的,比如当当网的elastic-job就是基于quartz二次开发之后的分布式调度解决方案。
使用Quartz可以很方便地与Spring集成,并且支持动态添加任务和集群。但是,Quartz使用起来也比较麻烦,API繁琐。
并且,Quzrtz并没有内置UI管理控制台,不过你可以使用quartzui这个开源项目来解决这个问题。
另外,Quartz虽然也支持分布式任务。但是,它是在数据库层面,通过数据库的锁机制做的,有非常多的弊端比如系统侵入性严重、节点负载不均衡。有点伪分布式的味道。
优缺点总结:
优点:可以与Spring集成,并且支持动态添加任务和集群。
缺点:分布式支持不友好,没有内置UI管理控制台、使用麻烦(相比于其他同类型框架来说)
Elastic-JobElastic-Job是当当网开源的一个基于Quartz和ZooKeeper的分布式调度解决方案,由两个相互独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成,一般我们只要使用Elastic-Job-Lite就好。
ElasticJob支持任务在分布式场景下的分片和高可用、任务可视化管理等功能。
ElasticJob-Lite的架构设计如下图所示:
从上图可以看出,Elastic-Job没有调度中心这一概念,而是使用ZooKeeper作为注册中心,注册中心负责协调分配任务到不同的节点上。
Elastic-Job中的定时调度都是由执行器自行触发,这种设计也被称为去中心化设计(调度和处理都是执行器单独完成)。
@Component@ElasticJobConf(name="dayJob",cron="0/10****?",shardingTotalCount=2,shardingItemParameters="0=AAAA,1=BBBB",description="简单任务",failover=true)publicclassTestJobimplementsSimpleJob{@Overridepublicvoidexecute(ShardingContextshardingContext){("TestJob任务名:【{}】,片数:【{}】,param=【{}】",(),(),());}}相关地址:
Github地址:。
官方网站:。
优缺点总结:
优点:可以与Spring集成、支持分布式、支持集群、性能不错
缺点:依赖了额外的中间件比如Zookeeper(复杂度增加,可靠性降低、维护成本变高)
XXL-JOBXXL-JOB于2015年开源,是一款优秀的轻量级分布式任务调度框架,支持任务可视化管理、弹性扩容缩容、任务失败重试和告警、任务分片等功能,
根据XXL-JOB官网介绍,其解决了很多Quartz的不足。
XXL-JOB的架构设计如下图所示:
从上图可以看出,XXL-JOB由调度中心和执行器两大部分组成。调度中心主要负责任务管理、执行器管理以及日志管理。执行器主要是接收调度信号并处理。另外,调度中心进行任务调度时,是通过自研RPC来实现的。
不同于Elastic-Job的去中心化设计,XXL-JOB的这种设计也被称为中心化设计(调度中心调度多个执行器执行任务)。
和Quzrtz类似XXL-JOB也是基于数据库锁调度任务,存在性能瓶颈。不过,一般在任务量不是特别大的情况下,没有什么影响的,可以满足绝大部分公司的要求。
不要被XXL-JOB的架构图给吓着了,实际上,我们要用XXL-JOB的话,只需要重写IJobHandler自定义任务执行逻辑就可以了,非常易用!
@JobHandler(value="myApiJobHandler")@ComponentpublicclassMyApiJobHandlerextsIJobHandler{@OverridepublicReturnTStringexecute(Stringparam)throwsException{//;}}还可以直接基于注解定义任务。
@XxlJob("myAnnotationJobHandler")publicReturnTStringmyAnnotationJobHandler(Stringparam)throwsException{//;}相关地址:
Github地址:。
官方介绍:。
优缺点总结:
优点:开箱即用(学习成本比较低)、与Spring集成、支持分布式、支持集群、内置了UI管理控制台。
缺点:不支持动态添加任务(如果一定想要动态创建任务也是支持的,参见:xxl-jobissue277)。
PowerJob由于SchedulerX属于人民币产品,我这里就不过多介绍。PowerJob官方也对比过其和QuartZ、XXL-JOB以及SchedulerX。
总结这篇文章中,我主要介绍了:
定时任务的相关概念:为什么需要定时任务、定时任务中的核心角色、分布式定时任务。
定时任务的技术选型:XXL-JOB2015年推出,已经经过了很多年的考验。XXL-JOB轻量级,并且使用起来非常简单。虽然存在性能瓶颈,但是,在绝大多数情况下,对于企业的基本需求来说是没有影响的。PowerJob属于分布式任务调度领域里的新星,其稳定性还有待继续考察。ElasticJob由于在架构设计上是基于Zookeeper,而XXL-JOB是基于数据库,性能方面的话,ElasticJob略胜一筹。
这篇文章并没有介绍到实际使用,但是,并不代表实际使用不重要。我在写这篇文章之前,已经动手写过相应的Demo。像Quartz,我在大学那会就用过。不过,当时用的是Spring。为了能够更好地体验,我自己又在SpringBoot上实际体验了一下。如果你并没有实际使用某个框架,就直接说它并不好用的话,是站不住脚的。