使用Java实现定时任务

一、定时任务是什么?

定时任务在实际的开发中是特别常见的,每天的凌晨公司会进行数据备份和汇总,这些步骤总不能让我们运维每天凌晨去手动备份吧,所以就写一个定时任务让机器每天定时去执行

二、实现方式(一):使用JDK自带的Timer来实现定时任务

代码如下(示例):

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * JDK自带的定时任务
 */
public class MyTimerTask {
    
    
    public static void main(String[] args) {
    
    
        TimerTask task = new TimerTask(){
    
    
            @Override
            public void run() {
    
    
                System.out.println("执行任务"+new Date());
            }
        };
        Timer timer=new Timer();
        //添加执行任务,延迟1s执行,每10s执行一次
        timer.schedule(task,1000,10000);
    }
}

执行结果:

执行任务Thu Oct 14 11:25:54 CST 2021
执行任务Thu Oct 14 11:26:04 CST 2021
执行任务Thu Oct 14 11:26:14 CST 2021
执行任务Thu Oct 14 11:26:24 CST 2021

问题1:这种方式有一个缺点,执行时候会影响其他业务.

代码如下(示例):

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;

public class MyTimerTaskA {
    public static void main(String[] args) {
        // 定义任务 1
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("进入 timerTask 1:" + new Date());
                try {
                    // 休眠 5 秒
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("任务 1:" + new Date());
            }
        };
        // 定义任务 2
        TimerTask timerTask2 = new TimerTask() {
            @Override
            public void run() {
                System.out.println("任务 2:" + new Date());
            }
        };
        // 计时器
        Timer timer = new Timer();
        // 添加执行任务(延迟 1s 执行,每 3s 执行一次)
        timer.schedule(timerTask, 1000, 3000);
        timer.schedule(timerTask2, 1000, 3000);
    }
}

执行结果

进入 timerTask 1:Thu Oct 14 11:30:28 CST 2021
任务 1:Thu Oct 14 11:30:33 CST 2021
任务 2:Thu Oct 14 11:30:33 CST 2021*
进入 timerTask 1:Thu Oct 14 11:30:33 CST 2021
任务 1:Thu Oct 14 11:30:38 CST 2021
进入 timerTask 1:Thu Oct 14 11:30:38 CST 2021
任务 1:Thu Oct 14 11:30:43 CST 2021
任务 2:Thu Oct 14 11:30:43 CST 2021*
  • 当任务1运行期间超过设置的间隔时,任务2也会跟着延迟执行,任务一是延迟了5s,但任务2延迟了10s执行下一次(和原定时间不符)

问题2:任务异常影响其他任务.

代码如下(示例):

package com.yf.timer;

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class MyTimerTaskB {
    public static void main(String[] args) {
        // 定义任务 1
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("进入 timerTask 1:" + new Date());
                // 模拟异常
                int num = 1 / 0;
                System.out.println("任务 1:" + new Date());
            }
        };
        // 定义任务 2
        TimerTask timerTask2 = new TimerTask() {
            @Override
            public void run() {
                System.out.println("任务 2:" + new Date());
            }
        };
     
        Timer timer = new Timer();
        // 添加执行任务(延迟 1s 执行,每 3s 执行一次)
        timer.schedule(timerTask, 1000, 3000);
        timer.schedule(timerTask2, 1000, 3000);
    }
}

执行结果

进入 timerTask 1:Thu Oct 14 11:34:16 CST 2021
Exception in thread "Timer-0" java.lang.ArithmeticException: / by zero
	at com.yf.timer.MyTimerTaskB$1.run(MyTimerTaskB.java:15)
	at java.base/java.util.TimerThread.mainLoop(Timer.java:556)
	at java.base/java.util.TimerThread.run(Timer.java:506)

小结:

Timer类简单易用,因为是我们JDK自带的工具类,但缺点就是以上两个问题,当执行时间过长,或者任务异常,会影响其他业务的调度,所以根据环境来选择适当的运用

三、实现方式(二):ScheduledExecutorService

ScheduledExecutorService 可以解决 Timer 任务之间相应影响的缺点,首先我们来测试一个任务执行时间过长,会不会对其他任务造成影响

扫描二维码关注公众号,回复: 13473802 查看本文章

代码如下(示例):

import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class MyScheduledExecutorService {
    
    
    public static void main(String[] args) {
    
    
        //创建任务队列
        ScheduledExecutorService s = Executors.newScheduledThreadPool(10);
        //执行任务 1
        s.scheduleAtFixedRate(()->{
    
    
            System.out.println("进入 Scheduc:"+new Date());
            //线程休眠5s
            try {
    
    
                Thread.sleep(5000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("开始任务1:"+new Date());
        },1,3, TimeUnit.SECONDS);// 1s 后开始执行,每 3s 执行一次
        //执行任务2
        s.scheduleAtFixedRate(()->{
    
    
            System.out.println("开始任务2:"+new Date());
        },1,3, TimeUnit.SECONDS);
    }
}

执行结果

进入 Scheduc:Thu Oct 14 11:44:00 CST 2021
开始任务2:Thu Oct 14 11:44:00 CST 2021
开始任务2:Thu Oct 14 11:44:03 CST 2021
开始任务1:Thu Oct 14 11:44:05 CST 2021
进入 Scheduc:Thu Oct 14 11:44:05 CST 2021
开始任务2:Thu Oct 14 11:44:06 CST 2021
开始任务2:Thu Oct 14 11:44:09 CST 2021
开始任务1:Thu Oct 14 11:44:10 CST 2021

从结果反映出,任务二不会因为任务一的延迟而影响,因此使用 ScheduledExecutorService 可以避免任务执行时间过长对其他任务造成的影响。

ScheduledExecutorService 也可以解决任务异常而导致任务二无法执行

代码如下(示例):

import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class MyScheduledExecutorServiceA {
    public static void main(String[] args) {
        // 创建任务队列
        ScheduledExecutorService scheduledExecutorService =
                Executors.newScheduledThreadPool(10);
        // 执行任务 1
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("进入 Schedule:" + new Date());
            // 模拟异常
            int num = 1 / 0;
            System.out.println("任务1:" + new Date());
        }, 1, 3, TimeUnit.SECONDS);
        // 执行任务 2
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("任务2:" + new Date());
        }, 1, 3, TimeUnit.SECONDS);
    }
}

执行结果

任务2:Thu Oct 14 11:51:57 CST 2021
进入 Schedule:Thu Oct 14 11:51:57 CST 2021
任务2:Thu Oct 14 11:52:00 CST 2021
任务2:Thu Oct 14 11:52:03 CST 2021
任务2:Thu Oct 14 11:52:06 CST 2021

从上面结果可以反映出,任务2不会因为任务1的异常而无法执行

小结:

在单机生产环境下建议使用 ScheduledExecutorService 来执行定时任务,它是 JDK 1.5 之后自带的 API,因此使用起来也比较方便,并且使用 ScheduledExecutorService 来执行任务,不会造成任务间的相互影响。

三、实现方式(三):Spring Task

如果使用的是 Spring 或 Spring Boot 框架,可以直接使用 Spring Framework 自带的定时任务,使用上面两种定时任务的实现方式,很难实现设定了具体时间的定时任务,比如当我们需要每周五来执行某项任务时,但如果使用 Spring Task 就可轻松的实现此需求。

  • 以SpringBoot 为例实现定时任务只需要两步
  1. 开启定时任务
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling//开启定时任务
public class DemoApplication {
//内容根据情况而定,只要项目跑起来就行
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
  1. 添加定时任务
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component //把此类托管给Spring
public class TaskUtils {
    //添加定时任务
    @Scheduled(cron = "59 59 23 0 0 5") //cron表达式,每周五晚上23.59.59执行
    public void doTask(){
        System.out.println("我是定时任务,开始执行");
    }
}

Corn表达式
Spring Task 的实现需要使用 cron 表达式来声明执行的频率和规则,cron 表达式是由 6 位或者 7 位组成的(最后一位可以省略),每位之间以空格分隔,每位从左到右代表的含义如下:
在这里插入图片描述
(1)* :表示匹配该域的任意值。假如在Minutes域使用*, 即表示每分钟都会触发事件。

(2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。

(3)-:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次

(4)/:表示起始时间开始触发,然后每隔固定时间触发一次。例如在Minutes域使用5/20,则意味着应该是从5分开始每20分钟触发一次

(5),:表示列出枚举值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。

(6)L:表示最后,只能出现在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。

(7)W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。

(8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。

(9)#:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。

常见表达式例子

(1)0 0 2 1 * ? * 表示在每月的1日的凌晨2点调整任务

(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业

(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作

(4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点

(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时

(6)0 0 12 ? * WED 表示每个星期三中午12点

(7)0 0 12 * * ? 每天中午12点触发

(8)0 15 10 ? * * 每天上午10:15触发

(9)0 15 10 * * ? 每天上午10:15触发

(10)0 15 10 * * ? * 每天上午10:15触发

(11)0 15 10 * * ? 2005 2005年的每天上午10:15触发

(12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发

(13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发

(14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

(15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发

(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发

(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发

(18)0 15 10 15 * ? 每月15日上午10:15触发

(19)0 15 10 L * ? 每月最后一日的上午10:15触发

(20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发

(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发

(22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发

(23)0 0 3 1 * ? 每月月初3点

cron 表达式在线生成地址:https://cron.qqe2.com/

三、实现方式(四):分布式定时任务

上面的实现方式都是基于单击的,如果四分布式情况下可以借用Reids来实现定时任务

使用Redis实现延迟任务方式大体可分为两类:通过 ZSet 的方式和键空间通知的方式。

  1. 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); // 每秒查询一次
        }
    }
}
  1. 键空间通知

    我们可以通过 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);
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_51250404/article/details/120760427