计划任务

项目中经常有定时任务的需求,比如定时刷新缓存,定时发送邮件等,@Scheduled功能能够通过很简单的配置,实现定时任务调度。

同类技术比较

除了Spring自带的@Scheduled框架,Quartz是另一个定时任务调度框架。相对而言@Scheduled的优势是使用非常简单,Spring项目中直接就可以使用,缺点是缺乏对分布式的支持,如果是分布式的项目依赖大量定时任务调度,还是应该考虑用Quartz。这部分内容可以参考Quartz相关章节。

除了定时调度框架,对于类似“X小时后关闭订单”,或是“调用失败后每隔X分钟重新调用”等类似需求,我们应该优先考虑是否可以用消息队列中的“延迟队列”功能代替。过多的使用定时任务会产生复杂的任务依赖问题,一个功能需要的定时任务越多,系统稳定性越差!

编写定时任务

首先我们要在工程中开启定时任务功能,SpringBoot工程中在启动类上添加@EnableScheduling注解即可。

package com.gacfox.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

下面代码创建了一个最简单的定时任务,运行项目后,每隔1秒会在控制台打印一个包含线程ID的字符串。

package com.gacfox.demo;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class TestSchedule {

    @Scheduled(fixedDelay = 1000L)
    public void test() {
        System.out.println("Scheduled on thread [" + Thread.currentThread().getId() + "]");
    }
}

输出例子:

Scheduled on thread [16]
Scheduled on thread [16]
Scheduled on thread [16]
Scheduled on thread [16]
Scheduled on thread [16]

创建定时任务使用@Scheduled注解就可以实现了,上面代码不一定非要放在Web容器中,只要我们的JVM进程没有退出,Spring就会分派线程执行定时任务。但是要注意,Spring容器需要启动,且当前类需要定义为Spring容器中的Bean。

指定时间间隔

指定时间间隔有两种方式:

  • fixedDelay:上一次执行完成后延迟若干毫秒执行(比较常用)
  • fixedRate:定时若干毫秒执行

使用cron表达式

除了指定定时执行的间隔,更为复杂的场景我们可以使用cron表达式。

例子,每分钟从0秒开始每2秒执行一次:

@Scheduled(cron = "0/2 * * * * *")

要注意一点的是,这里使用的cron表达式和Linux的crontab规则有些不同,Linux的crontab更像是一个简化版,最明显的区别是不支持秒级控制,如果把这里的cron表达式写进Linux的crontab会报语法错误,这里不要搞混了。

串行执行和并行执行

下面例子代码中,我们创建了taskAtaskB两个定时任务。

package com.gacfox.demo;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class TestSchedule {

    @Async
    @Scheduled(fixedDelay = 1000L)
    public void taskA() {
        System.out.println("Scheduled taskA on thread [" + Thread.currentThread().getId() + "]");
    }

    @Async
    @Scheduled(fixedDelay = 1000L)
    public void taskB() {
        try {
            Thread.sleep(5000L);
            System.out.println("Scheduled taskB on thread [" + Thread.currentThread().getId() + "]");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

例子输出:

Scheduled taskA on thread [17]
Scheduled taskA on thread [19]
Scheduled taskA on thread [21]
Scheduled taskA on thread [23]
Scheduled taskA on thread [25]
Scheduled taskB on thread [18]

假设我们没有加上@Async注解,两个任务是在同一线程中串行执行的,也就是说,尽管我们希望taskA是每秒执行一次,但是它被taskB阻塞了,不可能每秒执行。

而加上@Async注解后,Spring会创建一个默认线程数为100的线程池,不同的任务会尽量在新的线程中执行而不互相干扰。

分布式锁

如果我们的应用节点是多实例部署的,就不得不考虑分布式锁的问题。比如,我们的程序要定时给相关人员发送一封邮件,而应用节点部署了3个实例,如果没有分布式锁的考虑,这三个实例可能会造成在规定时间内,发出三封邮件,这显然是不行的。

分布式锁有很多种实现方式,MySQL的排他锁、Redis、Zookeeper等均能实现,这里就不多做介绍了。

常见问题:任务重复执行

对于老式Spring工程,我们知道,SpringMVC会初始化一个Web子容器,它和Spring的配置文件也通常是分开的,子容器中一般只定义和Web相关的内容,如ViewResolver等。

如果将定时任务Bean在两个容器中重复加载,重复执行问题就出现了,这会造成所有Bean在Spring容器和SpringMVC容器中各初始化了一份。大多数情况即使这么配置程序也能恰好正常工作,但我们定时任务的Bean如果初始化两次,定时任务也会连续运行两次,一些诡异的系统问题就可能源于此。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap