深入探讨定时任务框架与技术选型:从Spring Task到分布式调度解决方案

最近有朋友询问关于定时任务的相关问题,因此我撰写了一篇文章,旨在总结定时任务的一些基本概念及其常见技术选型。我希望这篇文章能为大家提供参考和帮助!

定时任务的重要性

定时任务在多个业务场景中发挥着重要作用,以下是一些常见的例子:

  1. 在某个系统中,数据需要在凌晨进行备份。
  2. 某电商平台中,用户下单后半小时未支付的订单需自动取消。
  3. 某媒体聚合平台每10分钟动态抓取特定网站的数据。
  4. 在博客平台上,支持定时发送文章。
  5. 某基金平台每晚定期计算用户的当日收益并向用户推送最新数据。

这些场景都要求我们在特定时间执行特定操作,因此定时任务变得至关重要。

单机定时任务技术选型

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-LiteElastic-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());  
    }  
}  

相关地址:

优缺点总结:

  • 优点:与 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 作为新星,其稳定性仍待观察。