首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >定时任务最简单的3种实现方法(超好用)

定时任务最简单的3种实现方法(超好用)

作者头像
Bug开发工程师
发布于 2020-08-24 06:54:49
发布于 2020-08-24 06:54:49
5.6K00
代码可运行
举报
文章被收录于专栏:码农沉思录码农沉思录
运行总次数:0
代码可运行

定时任务在实际的开发中特别常见,比如电商平台 30 分钟后自动取消未支付的订单,以及凌晨的数据汇总和备份等,都需要借助定时任务来实现,那么我们本文就来看一下定时任务最简单的几种实现方式。

TOP 1:Timer

Timer 是 JDK 自带的定时任务执行类,无论任何项目都可以直接使用 Timer 来实现定时任务,所以 Timer 的优点就是使用方便,它的实现代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MyTimerTask {
    public static void main(String[] args) {
        // 定义一个任务
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("Run timerTask:" + new Date());
            }
        };
        // 计时器
        Timer timer = new Timer();
        // 添加执行任务(延迟 1s 执行,每 3s 执行一次)
        timer.schedule(timerTask, 1000, 3000);
    }
}

程序执行结果如下:

Run timerTask:Mon Aug 17 21:29:25 CST 2020 Run timerTask:Mon Aug 17 21:29:28 CST 2020 Run timerTask:Mon Aug 17 21:29:31 CST 2020

Timer 缺点分析

Timer 类实现定时任务虽然方便,但在使用时需要注意以下问题。

问题 1:任务执行时间长影响其他任务

当一个任务的执行时间过长时,会影响其他任务的调度,如下代码所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MyTimerTask {
    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("Run timerTask 1:" + new Date());
            }
        };
        // 定义任务 2
        TimerTask timerTask2 = new TimerTask() {
            @Override
            public void run() {
                System.out.println("Run timerTask 2:" + new Date());
            }
        };
        // 计时器
        Timer timer = new Timer();
        // 添加执行任务(延迟 1s 执行,每 3s 执行一次)
        timer.schedule(timerTask, 1000, 3000);
        timer.schedule(timerTask2, 1000, 3000);
    }
}

程序执行结果如下:

进入 timerTask 1:Mon Aug 17 21:44:08 CST 2020 Run timerTask 1:Mon Aug 17 21:44:13 CST 2020 Run timerTask 2:Mon Aug 17 21:44:13 CST 2020 进入 timerTask 1:Mon Aug 17 21:44:13 CST 2020 Run timerTask 1:Mon Aug 17 21:44:18 CST 2020 进入 timerTask 1:Mon Aug 17 21:44:18 CST 2020 Run timerTask 1:Mon Aug 17 21:44:23 CST 2020 Run timerTask 2:Mon Aug 17 21:44:23 CST 2020 进入 timerTask 1:Mon Aug 17 21:44:23 CST 2020

从上述结果中可以看出,当任务 1 运行时间超过设定的间隔时间时,任务 2 也会延迟执行。 原本任务 1 和任务 2 的执行时间间隔都是 3s,但因为任务 1 执行了 5s,因此任务 2 的执行时间间隔也变成了 10s(和原定时间不符)。

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

使用 Timer 类实现定时任务时,当一个任务抛出异常,其他任务也会终止运行,如下代码所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MyTimerTask {
    public static void main(String[] args) {
        // 定义任务 1
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("进入 timerTask 1:" + new Date());
                // 模拟异常
                int num = 8 / 0;
                System.out.println("Run timerTask 1:" + new Date());
            }
        };
        // 定义任务 2
        TimerTask timerTask2 = new TimerTask() {
            @Override
            public void run() {
                System.out.println("Run timerTask 2:" + new Date());
            }
        };
        // 计时器
        Timer timer = new Timer();
        // 添加执行任务(延迟 1s 执行,每 3s 执行一次)
        timer.schedule(timerTask, 1000, 3000);
        timer.schedule(timerTask2, 1000, 3000);
    }
}

程序执行结果如下:

进入 timerTask 1:Mon Aug 17 22:02:37 CST 2020 Exception in thread "Timer-0" java.lang.ArithmeticException: / by zero at com.example.MyTimerTask$1.run(MyTimerTask.java:21) at java.util.TimerThread.mainLoop(Timer.java:555) at java.util.TimerThread.run(Timer.java:505) Process finished with exit code 0

Timer 小结

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

TOP 2:ScheduledExecutorService

ScheduledExecutorService 也是 JDK 1.5 自带的 API,我们可以使用它来实现定时任务的功能,也就是说 ScheduledExecutorService 可以实现 Timer 类具备的所有功能,并且它可以解决了 Timer 类存在的所有问题。

ScheduledExecutorService 实现定时任务的代码示例如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MyScheduledExecutorService {
    public static void main(String[] args) {
        // 创建任务队列
        ScheduledExecutorService scheduledExecutorService =
                Executors.newScheduledThreadPool(10); // 10 为线程数量
  // 执行任务
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("Run Schedule:" + new Date());
        }, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
    }
}

程序执行结果如下:

Run Schedule:Mon Aug 17 21:44:23 CST 2020 Run Schedule:Mon Aug 17 21:44:26 CST 2020 Run Schedule:Mon Aug 17 21:44:29 CST 2020

ScheduledExecutorService 可靠性测试

① 任务超时执行测试

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MyScheduledExecutorService {
    public static void main(String[] args) {
        // 创建任务队列
        ScheduledExecutorService scheduledExecutorService =
                Executors.newScheduledThreadPool(10);
        // 执行任务 1
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("进入 Schedule:" + new Date());
            try {
                // 休眠 5 秒
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Run Schedule:" + new Date());
        }, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
        // 执行任务 2
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("Run Schedule2:" + new Date());
        }, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
    }
}

程序执行结果如下:

Run Schedule2:Mon Aug 17 11:27:55 CST 2020 进入 Schedule:Mon Aug 17 11:27:55 CST 2020 Run Schedule2:Mon Aug 17 11:27:58 CST 2020 Run Schedule:Mon Aug 17 11:28:00 CST 2020 进入 Schedule:Mon Aug 17 11:28:00 CST 2020 Run Schedule2:Mon Aug 17 11:28:01 CST 2020 Run Schedule2:Mon Aug 17 11:28:04 CST 2020

从上述结果可以看出,当任务 1 执行时间 5s 超过了执行频率 3s 时,并没有影响任务 2 的正常执行,因此使用 ScheduledExecutorService 可以避免任务执行时间过长对其他任务造成的影响。

② 任务异常测试

接下来我们来测试一下 ScheduledExecutorService 在一个任务异常时,是否会对其他任务造成影响,测试代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MyScheduledExecutorService {
    public static void main(String[] args) {
        // 创建任务队列
        ScheduledExecutorService scheduledExecutorService =
                Executors.newScheduledThreadPool(10);
        // 执行任务 1
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("进入 Schedule:" + new Date());
            // 模拟异常
            int num = 8 / 0;
            System.out.println("Run Schedule:" + new Date());
        }, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
        // 执行任务 2
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("Run Schedule2:" + new Date());
        }, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
    }
}

程序执行结果如下:

进入 Schedule:Mon Aug 17 22:17:37 CST 2020 Run Schedule2:Mon Aug 17 22:17:37 CST 2020 Run Schedule2:Mon Aug 17 22:17:40 CST 2020 Run Schedule2:Mon Aug 17 22:17:43 CST 2020

从上述结果可以看出,当任务 1 出现异常时,并不会影响任务 2 的执行

ScheduledExecutorService 小结

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

TOP 3:Spring Task

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

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

  1. 开启定时任务;
  2. 添加定时任务。

具体实现步骤如下。

① 开启定时任务

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@SpringBootApplication
@EnableScheduling // 开启定时任务
public class DemoApplication {
    // do someing
}

② 添加定时任务

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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("我是定时任务~");
    }
}

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

Cron 表达式

Spring Task 的实现需要使用 cron 表达式来声明执行的频率和规则,cron 表达式是由 6 位或者 7 位组成的(最后一位可以省略),每位之间以空格分隔,每位从左到右代表的含义如下:

其中 * 和 ? 号都表示匹配所有的时间。

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

知识扩展:分布式定时任务

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

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

① ZSet 实现方式

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 的命令手动开启,开启之后定时任务的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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);
    }
}

点个在看支持我吧,转发就更好了

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-08-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农沉思录 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Typora使用Upgit对接EasyImage2.0图床
request_url处填写上文中蓝框框出的host部分,token处填写上文中蓝框框出的token部分。
huofo
2023/03/16
3720
Typora使用Upgit对接EasyImage2.0图床
个人博客搭建(3):Typora+PicGo+Github/Gitee搭建免费稳定的图床
注:在安装的时候安装目录千万不能选C:\Program Files\下的任何地方,因为PicGo无法解析这一路径,如果你不知道安装在哪里的话,选择仅为我安装,否则在设置Typora时会出现错误:
kif
2023/03/10
8490
PicGo:搭建图床
传送门:https://github.com/Molunerfinn/PicGo/releases
张小驰出没
2021/04/15
1.6K0
PicGo:搭建图床
Typora集成PicGo图床服务
给大家推送一波福利,新版本的Markdown写作利器——Typora,集成了PicGo服务,文章插入图片即可通过PicGo服务上传到图床内。废话不多说,直接开门见山!
YangAir
2020/04/29
7480
Typora + PicGo + Gitee/GitHub 免费搭建个人图床
写了将近一年多博客,之前半年都是用 富文本 的方式来写博客。直到遇到了一些博友,听说他们都是用 Markdown 格式来写博客。结果,我就放弃了富文本模式,见仁见智,我是觉得真的很难用。
Lucifer三思而后行
2022/01/08
1.7K0
Typora + PicGo + Gitee/GitHub 免费搭建个人图床
Typora+PicGo+Gitee或github实现markdown自带图床效果
这里可以选择gitee,gitee由于众所周知的原因有时候会很慢,无奈选择gitee
星哥玩云
2022/05/28
6180
Typora+PicGo+Gitee或github实现markdown自带图床效果
Typora + PicGo + 两个免费图床,轻松输出技术文...
最近跟粉丝们私下聊的比较多,其中有一部分是在校的学生或刚刚毕业参加工作的朋友们,交流比较多的问题是如何提升自己的技术水平,感觉比较的迷茫;那么我给的建议就是多分享、多输出,用输出倒逼输入;
一行Java
2022/04/07
1.8K0
Typora + PicGo + 两个免费图床,轻松输出技术文...
使用码云 + PicGO + Typora搭建一个自动化图床
没有账号的注册一个账号,国人自己的平台,支持下,还是挺良心的,码云传送门,注册完毕新建仓库,都是中文,不做概述,看图操作即可
框架师
2021/03/05
5610
Typora+开源图床+Python构建你的便捷移动MarkDown工作站
今天给大家介绍一款效率神器:Typora。我们使用码云(Gitee)来存放我们的写的博客文章和写博客过程中产生的图片。使用Python自动化上传写博客过程中产生的图片到码云(Gitee)。通过Pyth
嘉美伯爵
2021/01/18
6490
使用Typora+PicGo实现图片自动上传到Gitee图床【无坑系列,保姆级图文教学】
我们在使用Typora编辑器时,会加上图片,有个弊端,只能在本地访问,你发送给别人就无法查看图片,当然可以导出pdf。小编这边的需求是这样的,自己搭建的一个博客系统,基于Hexo搭建的,这个是使用Markdown进行上传到博客系统中。这样图片就需要一个外网可以访问的地址,所以就想到了这个办法,方法很多,小编罗列一下:
掉发的小王
2022/07/11
4930
使用Typora+PicGo实现图片自动上传到Gitee图床【无坑系列,保姆级图文教学】
在Picgo上配置Cloudflare-R2图床
图床就是将图片上传到相关服务商或者个人服务器,通过上传文件的网络地址进行远程访问。可以方便快速的将图片插入到文章中,方便后续图片二次使用、迁移、分享。
慕阳MuYoung
2025/06/12
1610
搭建个人免费图床并实现粘贴即上传:GitHub + PicGo + Typora
  本文介绍基于GitHub平台与PicGo工具,构建免费且相对较为稳定的图床,并实现在Typora内撰写Markdown文档时,粘贴图片就可以将这一图片自动上传到搭建好的图床中的方法。
疯狂学习GIS
2023/12/19
1.6K0
搭建个人免费图床并实现粘贴即上传:GitHub + PicGo + Typora
使用腾讯云对象存储搭建图床
平常在学习一些东西或者研究一些东西的时候会整理一下然后发到博客网站上,然后编辑时候一般使用的都是markdown格式,然后图片的存储有时候就很闹心,开始用的公共图床,但是没几天不是图片失效了就是图床网站倒闭了,然后现在很多网站都加限制防外链的。就想着自己搭建个图床,费用不高,可玩性较高,最主要资源自主管控,没有那么多限制。
愷龍
2023/02/26
7.1K0
使用腾讯云对象存储搭建图床
picgo+typora+gitee制作图传详细笔记(踩坑心得)
这位博主文章不错,可以参考: 传送门 交流群:970353786 图床链接:https://gitee.com/dgzde567/images/tree/master/images 成功上传: 在这里插入图片描述
川川菜鸟
2021/10/18
5250
利用github+jsDelivr搭建图床
图床是什么?图床就是图片存放的地址,用来节省服务器的资源,也可以变相的提升网站加载速度 你是否正在遭遇以下问题: 1.在用静态博客网站写文章,图片不知怎么保存,保存在哪里 2.网上复制的心仪图片链接,用着用着某一天就失效了 3.特意花钱租个云服务器托管图片,划不来,而且上传操作好繁琐 4.市面上形形色色的免费图床,但都有时间期限,要么就是速度慢,存储空间小,有的还限流量 现在可以利用jsdelivr加速github仓库来实现图床,无论是否使用jsdelivr你都可以利用github搭建一个图床,但缺点是国内加载速度非常慢,严重影响我们的需求,为此我们还需要利用jsdelivr的cdn加速,jsdelivr在国内的节点有上百个,这样我们就得到了一个访问速度贼快,且免费,空间无限的一个图床。 下面就是教程了,有疑问有错误请评论指出,谢谢,仅以此文章帮助烂记性的自己和正在观看此博客的你。
科技怪物君
2021/08/10
1.7K0
利用github+jsDelivr搭建图床
一款简约美观图片压缩自动转换WEBP格式图床程序
一个针对自用需求开发的图床程序,拥有高效的图片压缩功能和简洁美观的前台、后台,项目由几个简单的文件组成。采用简单高效的方式进行图片压缩,支持自定义压缩率和尺寸。 帮助大家减少图片储存、流量等方面的支出。
空木白博客
2024/09/11
1800
一款简约美观图片压缩自动转换WEBP格式图床程序
外链图床-PicGo-Gitee
所使用的工具为PicGo、Gitee、Typora. 在这之前,我一般在VSCode上面写Markdown文档,但是现在开始添加图片到博客中去,转而使用Typora,以便能够更好的契合PicGo使用(插入直接上传),而VSCode版本的PicGo插件暂时不支持Gitee图床的插件。
全栈程序员站长
2022/06/25
7590
外链图床-PicGo-Gitee
简单图床,真的简单!EasyImage搭建/使用教程
GitHub:icret/EasyImages2.0: 简单图床 – 一款功能强大无数据库的图床 2.0版 (github.com)
夜梦星尘
2024/08/20
3620
简单图床,真的简单!EasyImage搭建/使用教程
Typora gitee图床迁移github图床教程(图文详细)
 因此为了保证之前typora文档中的图片正常显示,我产生了将gitee图床转换为GitHub图床的想法。
timerring
2022/07/20
8710
Typora gitee图床迁移github图床教程(图文详细)
Gitee + Typora,搭建自己的免费图床!
对于写博客的朋友们来讲,图床这个东西一定不会陌生,而且在一定程度上也给大家造成过一定困扰。
村雨遥
2021/03/15
3.2K5
推荐阅读
相关推荐
Typora使用Upgit对接EasyImage2.0图床
更多 >
LV.2
这个人很懒,什么都没有留下~
目录
  • TOP 1:Timer
    • Timer 缺点分析
      • 问题 1:任务执行时间长影响其他任务
      • 问题 2:任务异常影响其他任务
    • Timer 小结
  • TOP 2:ScheduledExecutorService
    • ScheduledExecutorService 可靠性测试
      • ① 任务超时执行测试
      • ② 任务异常测试
    • ScheduledExecutorService 小结
  • TOP 3:Spring Task
    • ① 开启定时任务
    • ② 添加定时任务
    • Cron 表达式
  • 知识扩展:分布式定时任务
    • ① ZSet 实现方式
    • ② 键空间通知
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档