定时任务

定时任务

什么是定时任务

我们可以先思考一下下面业务场景的解决方案:

  • 某电商系统需要在每天上午10点,下午3点,晚上8点发放一批优惠券。
  • 某银行系统需要在信用卡到期还款日的前三天进行短信提醒。
  • 某财务系统需要在每天凌晨0:10结算前一天的财务数据,统计汇总。
  • 12306会根据车次的不同,而设置某几个时间点进行分批放票。
  • 某网站为了实现天气实时展示,每隔5分钟就去天气服务器获取最新的实时天气信息。

Timer

java.util.Timer是 JDK 1.3 开始就已经支持的一种定时任务的实现方式

Timer 内部使用一个叫做 TaskQueue 的类存放定时任务,它是一个基于最小堆实现的优先级队列TaskQueue 会按照任务距离下一次执行时间的大小将任务排序,保证在堆顶的任务最先执行。这样在需要执行任务时,每次只需要取出堆顶的任务运行即可!

Timer 使用起来比较简单,通过下面的方式我们就能创建一个 1s 之后执行的定时任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);


//输出:
当前时间: Fri May 28 15:18:47 CST 2024n线程名称: main
当前时间: Fri May 28 15:18:48 CST 2024n线程名称: Timer

不过其缺陷较多,比如一个 Timer 一个线程,这就导致 Timer 的任务的执行只能串行执行,一个任务执行时间过长的话会影响其他任务(性能非常差),再比如发生异常时任务直接停止(Timer 只捕获了 InterruptedException )。

ScheduledExecutorService

ScheduledExecutorService 是一个接口,有多个实现类,比较常用的是 ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor 本身就是一个线程池,支持任务并发执行。并且,其内部使用 DelayedWorkQueue 作为任务队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 示例代码:
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();

输出:

当前时间: Sun Dec 22 22:31:50 CST 2024n线程名称: main
当前时间: Sun Dec 22 22:31:51 CST 2024n线程名称: pool-1-thread-1
当前时间: Sun Dec 22 22:31:52 CST 2024n线程名称: pool-1-thread-1
当前时间: Sun Dec 22 22:31:53 CST 2024n线程名称: pool-1-thread-2
当前时间: Sun Dec 22 22:31:54 CST 2024n线程名称: pool-1-thread-2
当前时间: Sun Dec 22 22:31:55 CST 2024n线程名称: pool-1-thread-2

DelayQueue

DelayQueue 是 JUC 包(java.util.concurrent)为我们提供的延迟队列,用于实现延时任务比如订单下单 15 分钟未支付直接取消。它是 BlockingQueue 的一种,底层是一个基于 PriorityQueue 实现的一个无界队列,是线程安全的

DelayQueueTimer/TimerTask 都可以用于实现定时任务调度,但是它们的实现方式不同。DelayQueue 是基于优先级队列和堆排序算法实现的,可以实现多个任务按照时间先后顺序执行;而 Timer/TimerTask 是基于单线程实现的,只能按照任务的执行顺序依次执行,如果某个任务执行时间过长,会影响其他任务的执行。另外,DelayQueue 还支持动态添加和移除任务,而 Timer/TimerTask 只能在创建时指定任务。

Spring Task

我们直接通过 Spring 提供的 @Scheduled 注解即可定义定时任务,非常方便!

1
2
3
4
5
6
7
/**
* 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 表达式是一定是要重点关注的。推荐一个在线 Cron 表达式生成器:http://cron.qqe2.com/

但是,Spring 自带的定时调度只支持单机,并且提供的功能比较单一。之前写过一篇文章:《5 分钟搞懂如何在 Spring Boot 中 Schedule Tasks》 ,不了解的小伙伴可以参考一下。

Spring Task 底层是基于 JDK 的 ScheduledThreadPoolExecutor 线程池来实现的。

优缺点总结:

  • 优点:简单,轻量,支持 Cron 表达式
  • 缺点:功能单一

Redis

Redis 是可以用来做延时任务的,基于 Redis 实现延时任务的功能无非就下面两种方案:

  1. Redis 过期事件监听
  2. Redisson 内置的延时队列

MQ

大部分消息队列,例如 RocketMQ、RabbitMQ,都支持定时/延时消息。定时消息和延时消息本质其实是相同的,都是服务端根据消息设置的定时时间在某一固定时刻将消息投递给消费者消费。

不过,在使用 MQ 定时消息之前一定要看清楚其使用限制,以免不适合项目需求,例如 RocketMQ 定时时长最大值默认为 24 小时且不支持自定义修改、只支持 18 个 Level 的延时并不支持任意时间。

优缺点总结:

  • 优点:可以与 Spring 集成、支持分布式、支持集群、性能不错
  • 缺点:功能性较差、不灵活、需要保障消息可靠性

分布式任务调度

通常任务调度的程序是集成在应用中的,比如:优惠卷服务中包括了定时发放优惠卷的的调度程序,结算服务中包括了定期生成报表的任务调度程序,由于采用分布式架构,一个服务往往会部署多个冗余实例来运行我们的业务,在这种分布式系统环境下运行任务调度,我们称之为分布式任务调度。

通常情况下,一个分布式定时任务的执行往往涉及到下面这些角色:

  • 任务:首先肯定是要执行的任务,这个任务就是具体的业务逻辑比如定时发送文章。
  • 调度器:其次是调度中心,调度中心主要负责任务管理,会分配任务给执行器。
  • 执行器:最后就是执行器,执行器接收调度器分派的任务并执行。

针对分布式任务调度的需求市场上出现了很多的产品

1)Elastic-job:当当网基于quartz二次开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,实现任务高可用以及分片。

2)Saturn:唯品会开源的一个分布式任务调度平台,可以全域统一配置,统一监控,任务高可用以及分片并发处理。它是在elastic-job基础之上改良出来的。

3)xxljob:大众点评的分布式任务调度平台,是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

4)TBSchedule:淘宝的一款非常优秀的高性能分布式调度框架,目前被应用于阿里、京东、支付宝、国美等很多互联网企业的流程调度系统中。

分布式任务调度的目标

不管是任务调度程序集成在应用程序中,还是单独构建的任务调度统,如果采用分布式调度任务的方式就相当于将任务调度程序分布式构建,这样就可以具有分布式系统的特点,并且提高任务的调度处理能力

并行任务调度:并行任务调度实现靠多线程,如果有大量任务需要调度,此时光靠多线程就会有瓶颈了,因为一台计算机CPU的处理能力是有限的。如果将任务调度程序分布式部署,每个结点还可以部署为集群,这样就可以让多台计算机共同去完成任务调度,我们可以将任务分割为若干个分片,由不同的实例并行执行,来提高任务调度的处理效率。

高可用:若某一个实例宕机,不影响其他实例来执行任务。

弹性扩容:当集群中增加实例就可以提高并执行任务的处理效率。

任务管理与监测:对系统中存在的所有定时任务进行统一的管理及监测。让开发人员及运维人员能够时刻了解任务执行情况从而做出快速的应急处理响应。

避免任务重复执行:当任务调度以集群方式部署,同一个任务调度可能会执行多次,比如电商系统中到点发优惠券的例子,就会发放多次优惠券,对公司造成很多损失,所以我们需要控制相同的任务在多个运行实例上只执行次,考虑采用下边的方法:

分布式锁:多个实例在任务执行前首先需要获取锁,如果获取失败那么久证明有其他服务已经再运行,如果获取成功那么证明没有服务在运行定时任务,那么就可以执行。

ZooKeeper选举:利用ZooKeeper对Leader实例执行定时任务,有其他业务已经使用了ZK,那么执行定时任务的时候判断自己是否是Leader,如果不是则不执行,如果是则执行业务逻辑,这样也能达到我们的目的。

Quartz

一个很火的开源任务调度框架,完全由 Java 写成。Quartz 可以说是 Java 定时任务领域的老大哥或者说参考标准,其他的任务调度框架基本都是基于 Quartz 开发的,比如当当网的elastic-job就是基于 Quartz 二次开发之后的分布式调度解决方案。

使用 Quartz 可以很方便地与 Spring 集成,并且支持动态添加任务和集群。但是,Quartz 使用起来也比较麻烦,API 繁琐。

并且,Quartz 并没有内置 UI 管理控制台,不过你可以使用 quartzui 这个开源项目来解决这个问题。

另外,Quartz 虽然也支持分布式任务。但是,它是在数据库层面,通过数据库的锁机制做的,有非常多的弊端比如系统侵入性严重、节点负载不均衡。有点伪分布式的味道。

优缺点总结:

  • 优点:可以与 Spring 集成,并且支持动态添加任务和集群。
  • 缺点:分布式支持不友好,不支持任务可视化管理、使用麻烦(相比于其他同类型框架来说)

我们将创建一个简单的作业(Job)类,它将包含实际要执行的任务:

1
2
3
4
5
6
7
8
9
10
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class SimpleJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("SimpleJob is executing. " + new Date());
}
}

然后,我们需要创建一个调度器(Scheduler)并配置触发器(Trigger),以便在特定的日历日期执行作业:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.DateBuilder.IntervalUnit;
import org.quartz.TriggerBuilder;

import java.util.Calendar;
import java.util.Date;

public class CalendarDemo {
public static void main(String[] args) throws SchedulerException {
// 创建调度器
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

// 定义作业详情
JobDetail job = JobBuilder.newJob(SimpleJob.class)
.withIdentity("myJob", "group1").build();

// 创建日历,这里我们使用Calendar类来设置特定的日期和时间
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.DAY_OF_MONTH, 15); // 设置日历的日期为每月的15号
calendar.set(Calendar.HOUR_OF_DAY, 10); // 设置小时
calendar.set(Calendar.MINUTE, 30); // 设置分钟

// 创建触发器,使用CalendarIntervalTriggerBuilder来设置基于日历的触发时间
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "group1")
.startAt(calendar.getTime()) // 设置触发器的开始时间为上面设置的日历时间
.withSchedule(CronScheduleBuilder.cronSchedule("0 0/30 * * * ?")) // 每30分钟执行一次
.build();

// 将作业和触发器添加到调度器中
scheduler.scheduleJob(job, trigger);

// 启动调度器
scheduler.start();
}
}

在这个示例中,我们创建了一个名为SimpleJob的作业,它将在控制台打印当前日期和时间。然后我们创建了一个Calendar实例来设置特定的日期和时间,这里设置为每月的15号上午10:30。接着,我们创建了一个Trigger,它将在我们设置的日历时间开始,并每30分钟执行一次作业。

Elastic-Job

ElasticJob 当当网开源的一个面向互联网生态和海量任务的分布式调度解决方案,由两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。

ElasticJob-Lite 和 ElasticJob-Cloud 两者的对比如下:

ElasticJob-Lite ElasticJob-Cloud
无中心化
资源分配 不支持 支持
作业模式 常驻 常驻 + 瞬时
部署依赖 ZooKeeper ZooKeeper + Mesos

ElasticJob 支持任务在分布式场景下的分片和高可用、任务可视化管理等功能。

image-20241223102915731

相关地址:

优缺点总结:

  • 优点:开箱即用(学习成本比较低)、与 Spring 集成、支持分布式、支持集群、支持任务可视化管理。
  • 缺点:不支持动态添加任务(如果一定想要动态创建任务也是支持的,参见:xxl-job issue277)。

XXL-JOB

XXL-JOB 于 2015 年开源,是一款优秀的轻量级分布式任务调度框架,支持任务可视化管理、弹性扩容缩容、任务失败重试和告警、任务分片等功能,

根据 XXL-JOB 官网介绍,其解决了很多 Quartz 的不足。

Quartz 作为开源作业调度中的佼佼者,是作业调度的首选。但是集群环境中 Quartz 采用 API 的方式对任务进行管理,从而可以避免上述问题,但是同样存在以下问题:

  • 问题一:调用 API 的的方式操作任务,不人性化;
  • 问题二:需要持久化业务 QuartzJobBean 到底层数据表中,系统侵入性相当严重。
  • 问题三:调度逻辑和 QuartzJobBean 耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务;
  • 问题四:quartz 底层以“抢占式”获取 DB 锁并由抢占成功节点负责运行任务,会导致节点负载悬殊非常大;而 XXL-JOB 通过执行器实现“协同分配式”运行任务,充分发挥集群优势,负载各节点均衡。

XXL-JOB 弥补了 quartz 的上述不足之处。

XXL-JOB 的架构设计如下图所示:

image-20241223102624386

从上图可以看出,XXL-JOB调度中心执行器 两大部分组成。调度中心主要负责任务管理、执行器管理以及日志管理。执行器主要是接收调度信号并处理。另外,调度中心进行任务调度时,是通过自研 RPC 来实现的。

不同于 Elastic-Job 的去中心化设计, XXL-JOB 的这种设计也被称为中心化设计(调度中心调度多个执行器执行任务)。

Quzrtz 类似 XXL-JOB 也是基于数据库锁调度任务,存在性能瓶颈。不过,一般在任务量不是特别大的情况下,没有什么影响的,可以满足绝大部分公司的要求。

不要被 XXL-JOB 的架构图给吓着了,实际上,我们要用 XXL-JOB 的话,只需要重写 IJobHandler 自定义任务执行逻辑就可以了,非常易用!

1
2
3
4
5
6
7
8
9
@JobHandler(value="myApiJobHandler")
@Component
public class MyApiJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String param) throws Exception {
//......
return ReturnT.SUCCESS;
}
}

还可以直接基于注解定义任务。

1
2
3
4
5
@XxlJob("myAnnotationJobHandler")
public ReturnT<String> myAnnotationJobHandler(String param) throws Exception {
//......
return ReturnT.SUCCESS;
}

相关地址:

优缺点总结:

  • 优点:开箱即用(学习成本比较低)、与 Spring 集成、支持分布式、支持集群、支持任务可视化管理。
  • 缺点:不支持动态添加任务(如果一定想要动态创建任务也是支持的,参见:xxl-job issue277)。

PowerJob

非常值得关注的一个分布式任务调度框架,分布式任务调度领域的新星。目前,已经有很多公司接入比如 OPPO、京东、中通、思科。

定时任务方案总结

单机定时任务的常见解决方案有 TimerScheduledExecutorServiceDelayQueue、Spring Task 和时间轮,其中最常用也是比较推荐使用的是时间轮。另外,这几种单机定时任务解决方案同样可以实现延时任务。

Redis 和 MQ 虽然可以实现分布式定时任务,但这两者本身不是专门用来做分布式定时任务的,它们并不提供较为完整和强大的分布式定时任务的功能。而且,两者不太适合执行周期性的定时任务,因为它们只能保证消息被消费一次,而不能保证消息被消费多次。因此,它们更适合执行一次性的延时任务,例如订单取消、红包撤回。实际项目中,MQ 延时任务用的更多一些,可以降低业务之间的耦合度。

Quartz、Elastic-Job、XXL-JOB 和 PowerJob 这几个是专门用来做分布式调度的框架,提供的分布式定时任务的功能更为完善和强大,更加适合执行周期性的定时任务。除了 Quartz 之外,另外三者都是支持任务可视化管理的。

XXL-JOB 2015 年推出,已经经过了很多年的考验。XXL-JOB 轻量级,并且使用起来非常简单。虽然存在性能瓶颈,但是,在绝大多数情况下,对于企业的基本需求来说是没有影响的。PowerJob 属于分布式任务调度领域里的新星,其稳定性还有待继续考察。ElasticJob 由于在架构设计上是基于 Zookeeper ,而 XXL-JOB 是基于数据库,性能方面的话,ElasticJob 略胜一筹。


定时任务
http://example.com/2024/07/28/项目/微服务/定时任务/
作者
PALE13
发布于
2024年7月28日
许可协议