在实际项目的开发中,常常会遇到需要定时任务来执行一些特殊操作,比如定时监控一些数据信息,亦或者凌晨进行数据备份或者数据升级。这些都需要定时任务来实现,那么接下来就让我们看看一些常见的定时任务的实现方式

Timer

要是要实现定时任务,最先想到的肯定是java自带的类,就是Timer类,其在JDK类库中主要负责计划任务的功能,也就是在指定的时间开始执行某一个任务,或者进行一些周期性的工作。

无论是什么项目都可以直接使用Timer来实现定时任务,其特点就是方便使用。

代码示例

public class MyTimerTask {
    public static void main(String[] args) {
        // 定义一个任务
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("运行定时任务:"+ new Date());
            }
        };
        // 计时器
        Timer timer = new Timer();
        // 添加执行任务(延迟1s执行,每三秒执行一次)
        timer.schedule(timerTask,1000,3000);
    }
}

使用方法相对来说很简单,就是先用TimerTask创建一个任务,在其中重写run方法,在其中就正常编写我们的定时任务。

然后就是使用Timer类中的shedule方法来运行定时任务,其第一个参数就是要定时的任务,第二个参数就是延时多长时间,第三个参数就是时间间隔。

运行结果

java 定时任务如何用代码触发 java写定时任务_redis

优缺点

优点

优点是简单,就是此方法是由java原生支持的,在任何项目中都可以使用,而且使用简单快捷

缺点
  • 任务执行时间长影响其他任务
  • 当一个任务执行时间过长时,会影响其他任务的调度。
  • 任务异常影响其他任务
  • 我们使用Timer类实现定时任务时,当一个任务抛出异常,其他任务也会终止运行。

ScheduleExecutorService

在我们编写Timer代码,idea贴心的给我们划线标红并且提醒我们可以使用ScheduleExecutorService来代替。

java 定时任务如何用代码触发 java写定时任务_java_02

在图中也很清晰的告诉了我们其比Timer使用起来的优势以及如何使用,可谓是十分贴心了。

ScheduleExecutorService是JDK1.5自带的API,我们可以使用它来实现定时任务的功能,也就是说Timer可以实现的使用ScheduleExecutorService也可以轻松实现,而且还解决了Timer存在的问题。

代码示例

public class MyScheduleExecutorService {
    public static void main(String[] args) {
        // 创建任务队列,并且设置线程数量为10
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        // 执行任务
        scheduledExecutorService.scheduleAtFixedRate(()->{
            System.out.println("执行定时任务"+ new Date());
        },1,3, TimeUnit.SECONDS);
    }
}
运行结果

java 定时任务如何用代码触发 java写定时任务_java_03

优缺点

其相对于Timer来说最大的改变就是任务超时执行时,不会影响另外的任务按时执行,虽然依旧会影响自己的任务按时执行。而且就算发生异常,也不会对其他任务产生影响。

Spring Task

终于介绍到了Spring了,前面的两个方法是都JKD自带的,接下来我们要介绍的Spring Task就是基于Spring框架的,其直接使用了Spring Framework自带的定时任务,相比于前两种方法来说,其也可以轻松的指定具体时间来执行任务。

定时任务步骤

以Spring Boot为例,实现定时任务只需要两步:

  • 开启定时任务
  • 添加定时任务
开启定时任务

开启定时任务,只需要在Spring Boot的启动类上声明@EnableScheduling即可,代码如下所示

@SpringBootApplication
@EnableScheduling // 开启定时任务
public class DemoApplication {
    // do someing
}
添加定时任务

定时任务的添加只需要使用@Schedule注解标注即可,如果有多个定时任务,可以常见多个@Scheduled注解标注的方法,代码如下所示:

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
// 委托给Spring IOC容器
@Component 
public class TaskUtils {
    // 添加定时任务
    @Scheduled(cron = "59 59 23 0 0 5") // cron 表达式,每周五 23:59:59 执行
    public void doTask(){
        System.out.println("执行定时任务");
    }
}

需要注意的定时任务是自动触发的无需手动干预,也就是说SpringBoot启动后会自动加载并执行定时任务。

Cron表达式

在上面我们使用Spring Task的实现中我们使用到了Cron表达式,用其来声明执行的频率和规则,cron表达式是由6位或者7位组成的,每位之间用空格分隔,每位从左到右代表的含义如下:秒、分、小时、日期、月份、星期、年(可选,留空)

分布式定时任务

上面说的三种方法都是关于单机定时任务的实现,如果是分布式环境可以使用Redis来实现定时任务

使用Redis实现延迟任务的方法大体可以分为两种:通过Zset的方式和键空间通知的方式

ZSet实现方式

通过ZSet实现定时任务的思路是,将定时任务存放到ZSet集合中,并且将过期时间存储到ZSet的Score字段中,然后通过一个无线循环来判断当前时间内是否有需要执行的定时任务,如果有则进行执行,代码如下所示

import redis.clients.jedis.Jedis;
import utils.JedisUtils;
import java.time.Instant;
import java.util.Set;

public class DelayQueueExample {
    // zset key
    private static final String _KEY = "myTaskQueue";
    
    public static void main(String[] args) throws InterruptedException {
        Jedis jedis = JedisUtils.getJedis();
        // 30s 后执行
        long delayTime = Instant.now().plusSeconds(30).getEpochSecond();
        jedis.zadd(_KEY, delayTime, "order_1");
        // 继续添加测试数据
        jedis.zadd(_KEY, Instant.now().plusSeconds(2).getEpochSecond(), "order_2");
        jedis.zadd(_KEY, Instant.now().plusSeconds(2).getEpochSecond(), "order_3");
        jedis.zadd(_KEY, Instant.now().plusSeconds(7).getEpochSecond(), "order_4");
        jedis.zadd(_KEY, Instant.now().plusSeconds(10).getEpochSecond(), "order_5");
        // 开启定时任务队列
        doDelayQueue(jedis);
    }

    /**
     * 定时任务队列消费
     * @param jedis Redis 客户端
     */
    public static void doDelayQueue(Jedis jedis) throws InterruptedException {
        while (true) {
            // 当前时间
            Instant nowInstant = Instant.now();
            long lastSecond = nowInstant.plusSeconds(-1).getEpochSecond(); // 上一秒时间
            long nowSecond = nowInstant.getEpochSecond();
            // 查询当前时间的所有任务
            Set<String> data = jedis.zrangeByScore(_KEY, lastSecond, nowSecond);
            for (String item : data) {
                // 消费任务
                System.out.println("消费:" + item);
            }
            // 删除已经执行的任务
            jedis.zremrangeByScore(_KEY, lastSecond, nowSecond);
            Thread.sleep(1000); // 每秒查询一次
        }
    }
}

键空间通知

我们可以通过Redis的键空间通知来实现定时任务,它的实现思路是给所有的定时任务一个过期时间,等到了过期一环路,我们通过订阅过期消息就能感知到定时任务需要被执行了,此时我们执行定时任务即可。

默认情况下Redis是不开启键空间通知的,需要我们通过config set notify-keyspace-events Ex的命令手动开启,开启以后定时任务的代码如下:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import utils.JedisUtils;

public class TaskExample {
    public static final String _TOPIC = "__keyevent@0__:expired"; // 订阅频道名称
    public static void main(String[] args) {
        Jedis jedis = JedisUtils.getJedis();
        // 执行定时任务
        doTask(jedis);
    }

    /**
     * 订阅过期消息,执行定时任务
     * @param jedis Redis 客户端
     */
    public static void doTask(Jedis jedis) {
        // 订阅过期消息
        jedis.psubscribe(new JedisPubSub() {
            @Override
            public void onPMessage(String pattern, String channel, String message) {
                // 接收到消息,执行定时任务
                System.out.println("收到消息:" + message);
            }
        }, _TOPIC);
    }
}

总结

实现定时任务我们常用三种方法,分别是:Timer、ScheduleExecutorService、Spring Task。其中各有特点

  • Timer

Timer类实现定时任务的优点是方便,因为它是JDK自定的定时任务。但是缺点也很明显,就是如果定时任务执行时间太长或者任务异常,会影响其他任务的调度,所以在生产环境中谨慎使用。

  • ScheduleExecutorService

在单机环境下建议使用ScheduleExecutorService来执行定时任务,它是JDK1.5以后自带的API,使用起来十分的方便,而且使用这个方法也不会造成任务之间相互影响。

  • Spring Task

Spring框架集成的,无需手动干预,可以灵活的指定具体时间