深入探讨定时任务框架与技术选型:从Spring Task到分布式调度解决方案
最近有朋友询问关于定时任务的相关问题,因此我撰写了一篇文章,旨在总结定时任务的一些基本概念及其常见技术选型。我希望这篇文章能为大家提供参考和帮助!
定时任务的重要性
定时任务在多个业务场景中发挥着重要作用,以下是一些常见的例子:
- 在某个系统中,数据需要在凌晨进行备份。
- 某电商平台中,用户下单后半小时未支付的订单需自动取消。
- 某媒体聚合平台每10分钟动态抓取特定网站的数据。
- 在博客平台上,支持定时发送文章。
- 某基金平台每晚定期计算用户的当日收益并向用户推送最新数据。
这些场景都要求我们在特定时间执行特定操作,因此定时任务变得至关重要。
单机定时任务技术选型
Timer
java.util.Timer
是自 JDK 1.3 以来就支持的一种定时任务实现方式。其内部使用 TaskQueue
类来存储定时任务,TaskQueue
是基于最小堆实现的优先级队列,能够按任务距离下一次执行的时间进行排序,从而保证最快的任务优先执行。
使用 Timer
创建一个1秒后执行的定时任务非常简单,如下所示:
// 示例代码:
TimerTask task = new TimerTask() {
public void run() {
System.out.println("当前时间: " + new Date() + "\n" +
"线程名称: " + Thread.currentThread().getName());
}
};
System.out.println("当前时间: " + new Date() + "\n" +
"线程名称: " + Thread.currentThread().getName());
Timer timer = new Timer("Timer");
long delay = 1000L;
timer.schedule(task, delay);
然而,Timer
的缺陷同样明显,比如每个 Timer
只能在单个线程中执行任务,若某个任务执行时间过长,将影响后续任务的执行。此外,发生异常时任务会直接停止(Timer
仅捕获 InterruptedException
)。
在 Timer
类的注释中提到:
* This class does not offer real-time guarantees: it schedules
* tasks using the <tt>Object.wait(long)</tt> method.
* Java 5.0 introduced the {@code java.util.concurrent} package and
* one of the concurrency utilities therein is the {@link
* java.util.concurrent.ScheduledThreadPoolExecutor
* ScheduledThreadPoolExecutor} which is a thread pool for
* repeatedly executing tasks at a given rate or delay.
这意味着,ScheduledThreadPoolExecutor
支持多线程执行定时任务,并且功能更加全面,是 Timer
的较好替代品。
ScheduledExecutorService
ScheduledExecutorService
是一个接口,有多个实现类,其中最常用的是 ScheduledThreadPoolExecutor
。
ScheduledThreadPoolExecutor
本身就是一个线程池,支持任务并发执行,并且内部使用 DelayQueue
作为任务队列。
// 示例代码:
TimerTask repeatedTask = new TimerTask() {
@SneakyThrows
public void run() {
System.out.println("当前时间: " + new Date() + "\n" +
"线程名称: " + Thread.currentThread().getName());
}
};
System.out.println("当前时间: " + new Date() + "\n" +
"线程名称: " + Thread.currentThread().getName());
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
long delay = 1000L;
long period = 1000L;
executor.scheduleAtFixedRate(repeatedTask, delay, period, TimeUnit.MILLISECONDS);
Thread.sleep(delay + period * 5);
executor.shutdown();
无论是使用 Timer
还是 ScheduledExecutorService
,均无法通过 Cron 表达式指定任务执行的具体时间。
Spring Task
使用 Spring 提供的 @Scheduled
注解即可轻松定义定时任务,非常方便!
/**
* cron:使用Cron表达式。 每分钟的1,2秒运行
*/
@Scheduled(cron = "1-2 * * * * ?")
public void reportCurrentTimeWithCronExpression() {
log.info("Cron Expression: The time is now {}", dateFormat.format(new Date()));
}
在我大学时期的一个 SSM 企业级项目中,便使用了 Spring Task 来实现定时任务。Spring Task 支持 Cron 表达式,可用于定时作业系统定义执行时间或频率,非常强大。通过 Cron 表达式,我们可以灵活设置任务的执行时间,例如每天或每月的特定时间执行。因此,要学习定时任务,掌握 Cron 表达式至关重要。推荐使用的在线 Cron 表达式生成器:http://cron.qqe2.com/。
然而,Spring 自带的定时调度只支持单机,并且功能相对单一。我曾撰写过一篇关于如何在 Spring Boot 中使用定时任务的文章,感兴趣的朋友可以参考。
Spring Task 的底层是基于 JDK 的 ScheduledThreadPoolExecutor
实现的。
优缺点总结:
- 优点:简单轻量,支持 Cron 表达式
- 缺点:功能较为单一
时间轮
时间轮是一种在多个高效系统中使用的定时任务调度技术,如 Kafka、Dubbo 和 ZooKeeper。时间轮本质上是一个环形队列,通常底层基于数组实现,每个元素(时间格)可以存放一个定时任务列表。
时间轮中的每个时间格代表了基本的时间跨度或精度。例如,若时间轮中每秒走一个时间格,则其最高精度为1秒。在创建3秒后执行的定时任务时,只需将任务放在索引为3的时间格中。
对于创建13秒后执行的定时任务,可以引入 圈数/轮数 的概念,将任务放在索引为3的时间格中,同时增加圈数标识。
时间轮非常适合任务数量较多的场景,其任务写入和执行的时间复杂度均接近 O(1)。
分布式定时任务技术选型
前述提到的定时任务解决方案适用于单机环境,处理相对简单的任务场景(如每天凌晨备份数据)。但在复杂场景中,需要支持任务的分片和高可用性时,就需借助分布式任务调度框架。
常见的分布式定时任务通常涉及三个关键角色:
- 任务:具体需要执行的业务逻辑,比如定时发送文章。
- 调度器:调度中心,负责任务管理并将任务分配给执行器。
- 执行器:接收调度器分派的任务并执行。
Quartz
作为一个广受欢迎的开源任务调度框架,Quartz 完全由 Java 编写,被认为是定时任务领域的标准,其它任务调度框架大多基于 Quartz 进行开发,比如当当网的 elastic-job
。
使用 Quartz 可以方便地与 Spring 集成,支持动态添加任务和集群功能,但在使用上较为繁琐,API 也相对复杂。同时,Quartz 并不内置管理控制台,用户可以通过开源项目 quartzui 解决这个问题。
尽管 Quartz 支持分布式任务,但其实现方式主要依赖数据库的锁机制,可能导致系统侵入性较强,节点负载不均衡,性能也受到一定影响。
优缺点总结:
- 优点:可与 Spring 集成,支持动态添加任务和集群。
- 缺点:分布式支持较为复杂,无内置 UI 管理控制台,使用相对繁琐。
Elastic-Job
由当当网开源,基于 Quartz 和 ZooKeeper 的分布式调度解决方案,包含两个独立子项目 Elastic-Job-Lite
和 Elastic-Job-Cloud
,一般使用 Elastic-Job-Lite
。
Elastic-Job 支持在分布式环境下的任务分片和高可用性以及任务的可视化管理。
Elastic-Job 的设计并不包含调度中心的概念,而是使用 ZooKeeper 作为注册中心,负责协调任务分配。
@Component
@ElasticJobConf(name = "dayJob", cron = "0/10 * * * * ?", shardingTotalCount = 2,
shardingItemParameters = "0=AAAA,1=BBBB", description = "简单任务", failover = true)
public class TestJob implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
log.info("TestJob任务名:【{}】, 片数:【{}】, param=【{}】",
shardingContext.getJobName(),
shardingContext.getShardingTotalCount(),
shardingContext.getShardingParameter());
}
}
相关地址:
- Github 地址:https://github.com/apache/shardingsphere-elasticjob。
- 官方网站:https://shardingsphere.apache.org/elasticjob/index_zh.html。
优缺点总结:
- 优点:与 Spring 集成、支持分布式和集群、性能良好。
- 缺点:依赖额外的中间件如 Zookeeper,增加了复杂度和维护成本。
XXL-JOB
XXL-JOB 自2015年开源以来,已成为优秀的轻量级分布式任务调度框架,支持可视化管理、弹性扩展、任务失败重试及告警等功能。
XXL-JOB 的架构设计如下:
XXL-JOB 由 调度中心 和 执行器 两个关键部分组成,调度中心主要负责任务、执行器和日志管理,而执行器则接收调度信号并处理任务。调度中心通过自研 RPC 实现任务调度。
与 Elastic-Job 的去中心化设计不同,XXL-JOB 的设计为中心化设计(调度中心负责调度多个执行器执行任务)。虽然基于数据库锁调度任务,但在任务量不大的情况下,通常能够满足大部分的企业需求。
对于使用 XXL-JOB,只需重写 IJobHandler
自定义任务执行逻辑即可,非常易用!
@JobHandler(value="myApiJobHandler")
@Component
public class MyApiJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String param) throws Exception {
//......
return ReturnT.SUCCESS;
}
}
同样可以基于注解定义任务。
@XxlJob("myAnnotationJobHandler")
public ReturnT<String> myAnnotationJobHandler(String param) throws Exception {
//......
return ReturnT.SUCCESS;
}
相关地址:
优缺点总结:
- 优点:开箱即用(学习成本低)、与 Spring 集成、支持分布式和集群、内置 UI 管理控制台。
- 缺点:不支持动态添加任务(但可以通过特定方式实现)。
PowerJob
作为分布式任务调度领域的潜力新星,PowerJob 已被多家企业引入使用。这一框架的创建背景相当有趣,其作者在阿里巴巴的实习经历中接触过内部的 SchedulerX(阿里云付费产品),因此着手自研 PowerJob,以防未来需求无法满足。
PowerJob 的更多信息可以通过访问作者的视频《我和我的任务调度中间件》了解到。
总结
在本文中,我主要介绍了:
- 定时任务的相关概念:为何需要定时任务、定时任务中的核心角色及分布式定时任务。
- 定时任务的技术选型:包括 XXL-JOB 和 PowerJob 的介绍,前者经过多年考验,轻量且易用,能满足绝大多数企业需求;而 PowerJob 作为新星,其稳定性仍待观察。