定时任务的几种实现方式

定时任务实现的几种方式:

1、JDK自带

  • (1)Timer:这是java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行。一般用的较少。
  • (2)ScheduledExecutorService:也jdk自带的一个类;是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。

2、Spring Task

  • Spring Task :Spring3.0以后自带的task,Spring Boot提供的用于定时任务控制的注解**@Scheduled注解**,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。

3、第三方任务调度框架

除了使用Spring框架提供的 @Scheduled 注解和SchedulingConfigurer接口外,还有许多第三方的任务调度库可供选择。这些库通常提供了更多的功能和灵活性,以满足各种复杂的任务调度需求。以下是一些常见的第三方任务调度库:

  • Quartz Scheduler:是一个功能强大且灵活的任务调度库,具有丰富的功能,如支持基于cron表达式的任务调度、集群支持、作业持久化等。它可以与Spring框架集成,并且被广泛应用于各种类型的任务调度应用程序中。
  • xxl-job:是一个分布式任务调度平台,提供了可视化的任务管理界面和多种任务调度方式,如单机任务、分布式任务、定时任务等。它支持任务执行日志、任务失败重试、动态调整任务执行策略等功能。
  • Elastic Job:是一个分布式任务调度框架,可以轻松实现分布式任务调度和作业执行。它提供了分布式任务执行、作业依赖关系、作业分片等功能,适用于大规模的分布式任务调度场景。
  • PowerJob:是一个开源的分布式任务调度框架,由阿里巴巴集团开发并开源。PowerJob 提供了分布式、高可用的任务调度能力,支持多种任务类型,如定时任务、延时任务、流程任务等。

目录

一、JDK自带

1、使用Timer

2、使用ScheduledExecutorService

[二、Spring Task](#二、Spring Task)

1、基础使用案例

2、参数说明简介

3、cron表达式

(1)cron参数配置描述

(2)cron通配符描述

4、cron案例

5、实现多任务并行(默认单线程)

6、局限性(不支持年份设定)


一、JDK自带

1、使用Timer

java 复制代码
/**
 * Timer:这是java自带的java.util.Timer类
 *
 * 这个类允许你调度一个java.util.TimerTask任务
 * 使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行
 * 【一般用的较少】
 */
@Configuration
public class JDKTimerTask {

	@Bean
	public void test(){
		TimerTask timerTask = new TimerTask() {
			@Override
			public void run() {
				System.err.println("task  run:"+ LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
			}
		};
		Timer timer = new Timer();
		//安排指定的任务在指定的时间开始进行重复的固定延迟执行。这里是每3秒执行一次
		timer.schedule(timerTask,10,3000);
	}
}

执行结果

2、使用ScheduledExecutorService

java 复制代码
/**
 * ScheduledExecutorService:也jdk自带的一个类;
 * 是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行
 * 也就是说,任务是并发执行,互不影响。
 */
@Configuration
public class ScheduledExecutorServiceTimerTask {

	@Bean
	public void test(){
		ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
		// 参数:1、任务体 2、首次执行的延时时间
		//      3、任务执行间隔 4、间隔时间单位
		service.scheduleAtFixedRate(()->System.out.println("task ScheduledExecutorService "+ LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName()), 0, 3, TimeUnit.SECONDS);

	}
}

执行结果:

二、Spring Task

@Scheduled注解是Spring Boot提供的用于定时任务控制的注解,主要用于控制任务在某个指定时间执行,或者每隔一段时间执行

@Scheduled需要配合**@EnableScheduling**使用。使用时,将@Scheduled注解放在待定时的方法名上方,将 @EnableScheduling放在项目主启动类类名上方。@Scheduled主要有三种配置执行时间的方式:cronfixedRatefixedDelay

注意:@Scheduled是不支持年份设置,spring quartz支持

1、基础使用案例

(1)主启动类增加注解**@EnableScheduling**:表明开启定时任务

java 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling //开启定时任务注解
public class TimedTaskDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(TimedTaskDemoApplication.class, args);
    }

}

(2)定时任务执行方法上增加注解**@Scheduled**:表明该方法为定时任务需要执行的内容

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;

@Component
@Slf4j
public class TaskDemo {

//    @Scheduled(cron = "0 0 4 ? * SAT")//每周六凌晨4:00
    @Scheduled(cron = "* * * * * *")//每秒执行一次
    private void doTask(){
        log.info("执行定时任务:" + new Date());
    }
}

2、参数说明简介

| 参数 | 说明 | |
| cron | 任务执行的cron表达式 | |
| zone | cron表达时解析使用的时区,默认为服务器的本地时区。 使用java.util.TimeZone#getTimeZone(String)方法解析 | GMT-8:00 |
| fixedRate | 固定速率 上一次任务执行开始到下一次执行开始的间隔时间固定,单位为ms。 若在调度任务执行时,上一次任务还未执行完毕,会加入worker队列,等待上一次执行完成后,马上执行下一次任务 | 1000 |
| fixedRateString | 与fixedRate一致,只是间隔时间使用java.time.Duration#parse解析 | 1000或PT1S |
| fixedDelay | 固定延迟 上一次任务执行结束到下一次执行开始的间隔时间固定,单位为ms。 | 1000 |
| fixedDelayString | 与fixedDelay一致,只是间隔时间使用java.time.Duration#parse解析 | 1000或PT1S |
| initialDelay | 首次延迟多长时间后执行,单位ms。 之后按照fixedRate、fixedRateString、fixedDelay、fixedDelayString指定的规则执行,需要指定其中一个规则。 注意:不能和cron一起使用 | 1000 |

initialDelayString 与initialDelay 一致,只是间隔时间使用java.time.Duration#parse解析 1000或PT1S

关于 fixedRate 和 fixedDelay的区别:

fixedRate 的间隔时间是上次任务开始后,开始计算时间间隔,达到指定时间间隔后,开始执行下一次任务

fixedDelay 的间隔时间是上次任务结束后,开始计算时间间隔,达到指定时间间隔后,开始执行下一次任务

例如:当一个任务需要统计大量数据,并根据不用用户生成不同文件报表,并将生成的报表推送到相应的用户下,该任务有补充机制。在执行该任务时,将使用大量时间时,此时使用 fixedRate 就会造成工作队列长时间堆积,同时,如果使用 fixedRate 的话,与 cron 的作用高度重合,基本可以用 cron 表达式替代,这个情况下,使用 fixedDelay 效果更好,这是在一个任务结束后,再次执行该任务进行重试,直到所有用户都拿到相应的文件。

3、cron表达式

cron@Scheduled的一个参数,是一个字符串,以空格隔开

复制代码
@Scheduled(cron = "{秒数} {分钟} {小时} {日期} {月份} {星期}")

(1)cron参数配置描述

| 单位 | 允许值 | 允许通配符 |
| 秒 | 0-59 | , - * / |
| 分钟 | 0-59 | , - * / |
| 小时 | 0-23 | , - * / |
| 日期 | 1-31 | , - * / ? L W |
| 月份 | 1-12或JAN-DEC(大小写均可) | , - * / ? |

星期 1-7或SUN-SAT(大小写均可) 注:星期日为每周第一天,所以1-7表示周末到周六 , - * / ? L #

(2)cron通配符描述

| 符号 | 含义 |
| * | 所有值,在秒字段上表示每秒执行,在月字段上表示每月执行 |
| ? | 不指定值,不需要关心当前指定的字段的值,比如每天都执行但不需要关心周几就可以把周的字段设为? |
| - | 区间或者范围,如秒的0-2 ,表示0秒、1秒、2秒都会触发 |
| , | 指定值,比如在0秒、20秒、25秒触发,可以把秒的字段设为0,20,25 |
| / | 递增触发,比如秒的字段上设0/3 ,表示从第0秒开始,每隔3秒触发 |
| L | 最后,只允许在日字段或周字段上,在日字段上使用L表示当月最后一天,在周字段上使用3L表示该月最后一个周二 |
| W | 只允许用在日字段上,表示距离最近的该日的工作日,工作日指的是周一至周五 |

# 只允许在周字段上,表示每月的第几个周几,如2#3 , 每月的第3个周二

说明:

在子表达式(分钟)里的"0/15"表示从第0分钟开始,每15分钟

在子表达式(分钟)里的"3/20"表示从第3分钟开始,每20分钟(它和"3,23,43")的含义一样
"?"字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值

当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为"?"
"L" 字符仅被用于天(月)和天(星期)两个子表达式,它是单词"last"的缩写

但是它在两个子表达式里的含义是不同的。

在天(月)子表达式中,"L"表示一个月的最后一天

在天(星期)自表达式中,"L"表示一个星期的最后一天,也就是SAT

如果在"L"前有具体的内容,它就具有其他的含义了

例如:"6L"表示这个月的倒数第6天,"FRIL"表示这个月的最一个星期五

注意:在使用"L"参数时,不要指定列表或范围,因为这会导致问题

4、cron案例

复制代码
"0 0 10,14,16 * * ?" 每天上午10点,下午2点,4点

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

"0 0 12 ? * WED" 表示每个星期三中午12点

"0 0 12 * * ?" 每天中午12点触发

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

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

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

"0 15 10 * * ? 2005" 2005年的每天上午10:15触发

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

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

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

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

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

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

"0 15 10 15 * ?" 每月15日上午10:15触发

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

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

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

5、实现多任务并行(默认单线程)

默认情况下如果存在多个定时任务方法是单线程同步执行

验证代码:创建两个定时任务,其中一个睡眠3秒钟

java 复制代码
@Component
@Slf4j
public class TaskDemo {

    @Scheduled(cron = "* * * * * *")//每秒执行一次
    private void doTask(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("执行定时任务1:" + new Date());
    }

    @Scheduled(cron = "* * * * * *")//每秒执行一次
    private void doTask2(){
        log.info("执行定时任务【2】:" + new Date());
    }
}

单线程验证结果:从执行时间和线程名可以看出两个定时任务是单线程,使用的是同一个线程执行的

增加配置类:异步任务配置类进行线程池的设置

java 复制代码
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executor;

/**
 * 异步任务配置类
 * 需要实现SchedulingConfigurer接口
 * 需要实现AsyncConfigurer接口
 */
@Configuration
public class AsynTaskConfig implements AsyncConfigurer,SchedulingConfigurer {

    // 定义池子的容量
    private static final int CRON_POOL_SIZE = 10;

    // 注册定时任务线程池
    @Bean
    public ThreadPoolTaskScheduler getThreadPoolTaskScheduler() {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        // 初始化线程池
        threadPoolTaskScheduler.initialize();
        // 设置池子容量
        threadPoolTaskScheduler.setPoolSize(CRON_POOL_SIZE);
        return threadPoolTaskScheduler;
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setTaskScheduler(getThreadPoolTaskScheduler());
    }


    //重写AsyncConfigurer的两个方法(异步的配置)
    @Override
    public Executor getAsyncExecutor() {
        return AsyncConfigurer.super.getAsyncExecutor();
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return AsyncConfigurer.super.getAsyncUncaughtExceptionHandler();
    }
}

异步多线程验证结果:从执行时间和线程名可以看出两个定时任务是多线程

6、局限性(不支持年份设定)

@Scheduled 的 cron 无法指定执行的年份

spring taks 不支持年位定时,它毕竟不是quartz,只是简单的定时框架,比起jdk Timer就加入了线程池而以.

一旦制定到年份,会存在问题,启动项目的时候,会一直报一个错误,大概的意思是你的定时任务将永远不会被执行,导致项目一直启动不了。

错误场景一:年份设置在第7位时报错,cron只支持到六位

错误场景二:年份设置在第6位时报错,cron第六位表示星期,内容范围只支持1-7

相关推荐
Chenyiax3 分钟前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH4 分钟前
Koa和Express的区别
后端
MariaH10 分钟前
Koa框架的使用
后端
luckdewei1 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某3 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy3 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom3 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
唐青枫7 小时前
Java JDBC 实战指南:从 Connection 到事务和连接池
java
用户1474853079747 小时前
CodeX使用Skill生成游戏美术和音乐资源,一分钟入门
后端
Melody1237 小时前
用 abort 中断 AI 流式请求,我之前做错了
后端