定时任务解决方案

定时任务概述

在很多应用中我们都是需要执行一些定时任务的,比如定时发送短信,定时统计数据,在实际使用中我们使用什么定时任务框架来实现我们的业务,定时任务使用中会遇到哪些坑,如何最大化的提高定时任务的性能。

我们这里主要介绍单机和分布式两大类的解决方案,并且简要介绍两类方案中的常见的应用组件或者框架的应用场景和基本的实现原理,重点分析下单机的定时任务的实现原理和优缺点。

为什么需要定时任务

下面是几个常见的定时任务场景

  1. 某系统凌晨要进行数据备份。
  2. 某媒体聚合平台,每 10 分钟动态抓取某某网站的数据为自己所用。
  3. 某博客平台,支持定时发送文章。
  4. 某基金平台,每晚定时计算用户当日收益情况并推送给用户最新的数据。

定时任务选型

单机定时任务

分布式的定时任务框架也是通过单机的原理而来,这里先介绍单机的几种实现方案,并且简单的对比分析

while+sleep方案

我们自己来实现一个定时任务,可以采用最简单的while循环加上一个sleep休眠方案,sleep是需要休眠的事件,下面就是我们事件的一个每隔5s休眠一次的案例

public class Scheduled1 {

    private static final long timeInterval = 5000;
    public static void main(String[] args) {

        new Thread(()->{
            while(true){
                System.out.println("定时任务每隔"+timeInterval+"毫秒执行一次");
                try {
                    Thread.sleep(timeInterval);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

 

实现还是很简单的,但是有一个问题,如果我们不仅仅只有定时5s的,还有3s、10s、20s的如何解决呢?不能每来一个不同的定时任务都需要新启动一个线程,这样会造成很多缺点:代码量巨大、开启线程很多占用内存、上下文切换频繁等

Timer定时器

定时计划任务功能在Java中主要使用的就是Timer对象,它在内部使用多线程的方式进行处理,所以它和多线程技术还是有非常大的关联的。在JDK中Timer类主要负责计划任务的功能,也就是在指定的时间开始执行某一个任务,但封装任务的类却是TimerTask类

创建定时任务

通过继承 TimerTask 类 并实现 run() 方法来自定义要执行的任务

public class TimeTask1 extends TimerTask {
    @Override
    public void run() {
        System.out.println("定时任务运行了");
    }
}

调度定时任务

通过执行Timer.schedule(TimerTask task,Date time) 在执行时间运行任务

public class TimeScheduled {
    private static final long timeInterval = 5000;
    public static void main(String[] args) {
        Timer timer = new Timer();
        //延时10毫秒,每隔5s执行一次
        timer.schedule(new TimeTask1(),10,timeInterval);
    }
}

我们发现和我们第一个方案差不多,但是Timer的方案更加科学高效,我们发现他是可以支持延时执行,并且是可以支持定点执行

缺点

比如一个 Timer 一个线程,这就导致 Timer 的任务的执行只能串行执行,一个任务执行时间过长的话会影响其他任务。

线程池方式

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

 

jdk自带的一个类是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响

代码案例
public class ScheduledExecutor {
    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
        service.scheduleAtFixedRate(() -> System.out.println("执行任务"), 10, 5, TimeUnit.SECONDS);
    }
}

不论是使用 Timer 还是 ScheduledExecutorService 都无法使用 Cron 表达式指定任务执行的具体时间。

springTask

SpringTask是Spring自主研发的轻量级定时任务工具,相比于Quartz更加简单方便,且不需要引入其他依赖即可使用

启动SpringTask

在配置类中添加一个@EnableScheduling注解即可开启SpringTask的定时任务

@SpringBootApplication
//启用定时任务的配置
@EnableScheduling
public class SpringTaskApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringTaskApplication.class);
    }
}
创建任务类

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

@Component
public class SpringTask {
    @Scheduled(cron = "0/5 * * * * ?")
    public void testTask() throws InterruptedException {
        System.out.println("执行SpringTask任务,时间:" + LocalDateUtils.getLocalDateTimeStr());
    }
}

 

支持Cron表达式

Spring Task 支持 Cron 表达式

Cron 表达式主要用于定时作业(定时任务)系统定义执行时间或执行频率的表达式,非常厉害,你可以通过 Cron 表达式进行设置定时任务每天或者每个月什么时候执行等等操作

推荐一个在线 Cron 表达式生成器:在线Cron表达式生成器

优缺点
  • 优点:简单,轻量,支持 Cron 表达式
  • 缺点 :功能单一
分布式定时任务

上面提到的一些定时任务的解决方案都是在单机下执行的,适用于比较简单的定时任务场景比如每天凌晨备份一次数据

如果我们需要一些高级特性比如支持任务在分布式场景下的分片和高可用的话,我们就需要用到分布式任务调度框架了

Quartz

Quartz是一个完全由Java编写的开源任务调度的框架,通过触发器设置作业定时运行规则,控制作业的运行时间,其中quartz集群通过故障切换和负载平衡的功能,能给调度器带来高可用性和伸缩性

 

quartz也是用的比较多的定时任务,很多分布式定时任务或者定制定时任务都是基于quartz来实现的,比如elastic-job就是借鉴quartz来实现的

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

Elastic-job是当当网张亮主导开发的分布式任务调度框架,结合zookeeper技术解决quartz框架在分布式系统中重复的定时任务导致的不可预见的错误,功能丰富强大,实现任务高可用以及分片

Elastic-Job 中的定时调度都是由执行器自行触发,这种设计也被称为去中心化设计(调度和处理都是执行器单独完成)。

功能列表

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

 

优缺点总结
  • 优点 :可以与 Spring 集成、支持分布式、支持集群、性能不错
  • 缺点 :依赖了额外的中间件比如 Zookeeper(复杂度增加,可靠性降低、维护成本变高)
XXL-JOB

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

功能列表

 

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

不同于 Elastic-Job 的去中心化设计, XXL-JOB 的采用了中心化设计(调度中心调度多个执行器执行任务)

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

优缺点总结:

  • 优点:开箱即用(学习成本比较低)、与 Spring 集成、支持分布式、支持集群、内置了 UI 管理控制台。
  • 缺点:不支持动态添加任务(如果一定想要动态创建任务也是支持的)。
组件对比

下图是常见的几个分布式定时任务的对比

featurequartzelastic-job-litexxl-job
依赖mysqljdk1.7+, zookeeper 3.4.6+ ,maven3.0.4+mysql ,jdk1.7+ , maven3.0+
HA多节点部署,通过竞争数据库锁来保证只有一个节点执行任务通过zookeeper的注册与发现,可以动态的添加服务器。支持水平扩容集群部署
任务分片支持支持
文档完善完善完善完善
管理界面支持支持
难易程度简单较复杂简单
公司OpenSymphony当当网个人
高级功能弹性扩容,多种作业模式,失效转移,运行状态收集,多线程处理数据,幂等性,容错处理,spring命名空间支持弹性扩容,分片广播,故障转移,Rolling实时日志,GLUE(支持在线编辑代码,免发布),任务进度监控,任务依赖,数据加密,邮件报警,运行报表,国际化
缺点没有管理界面,以及不支持任务分片等。不适用于分布式场景需要引入zookeeper , mesos, 增加系统复杂度, 学习成本较高调度中心通过获取 DB锁来保证集群中执行任务的唯一性, 如果短任务很多,随着调度中心集群数量增加,那么数据库的锁竞争会比较厉害,性能不好。
使用企业大众化产品,对分布式调度要求不高的公司大面积使用36氪,当当网,国美,金柚网,联想,唯品会,亚信,平安,猪八戒大众点评,运满满,优信二手车,拍拍贷

 

Logo

欢迎加入西安开发者社区!我们致力于为西安地区的开发者提供学习、合作和成长的机会。参与我们的活动,与专家分享最新技术趋势,解决挑战,探索创新。加入我们,共同打造技术社区!

更多推荐