定时任务(fixedDelay/fixedRate/cron)

文章详细介绍了Spring中@Scheduled注解的fixedRate和fixedDelay的使用和区别,以及如何避免任务阻塞,包括启用多线程和配置线程池的方法。同时,还讨论了cron表达式在定时任务中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、核心区别

1.实例

2.fixedDelay、fixedDelayString

3.fixedRate、fixedRateString

4.initialDelay、initialDelayString

5.zone

二、之前的笔记

三、如何避免fixedRate任务阻塞

1.多线程注解

2.配置Scheduling的线程池(implements SchedulingConfigurer)

四、cron

1.表达式语法

2.cron的函数式接口写法​​​​​​​


一、核心区别

1.实例

@Scheduled(
        initialDelayString = "${cas.serviceRegistry.schedule.startDelay:20000}",
        fixedDelayString = "${cas.serviceRegistry.schedule.repeatInterval:60000}"
)

上面摘自CAS源码:容器启动后延迟20秒再执行一次定时器,以后按fixedDelayString(同fixedDelay,不过fixedDelayString支持占位符)的规则每60秒执行一次

2.fixedDelayfixedDelayString

相同点:都表示上一次执行完毕时间点之后多长时间再执行,可与initialDelay搭配使用(若不与initialDelay搭配则项目启动时就会立马执行一次),不会阻塞(宜优先考虑使用)

区别点:后者支持占位符。

fixedDelay的间隔时间是从上次任务结束时计时的:

比如一个方法上设置了fixedDelay=5*1000,那么当该方法某一次执行结束后,开始计算时间,当时间达到5秒,就开始再次执行该方法,因此不会阻塞。

3.fixedRatefixedRateString

相同点:都表示上一次执行开始时间点之后多长时间再执行,可与initialDelay搭配使用(若不与initialDelay搭配则项目启动时就会立马执行一次),可能会阻塞

区别点:后者支持占位符。

fixedRate的间隔时间是从上次任务开始时计时的:

比如方法上设置fiexdRate=5*1000,执行该方法所花时间是2秒,那么3秒后就会再次执行该方法。如果任务执行时长超过设置的间隔时长,比如一个任务本来只需要花2秒就能执行完成,但是因为网络问题导致这个任务花了7秒才执行完成,那么后续的任务就会被阻塞,因为当定时任务开始时Spring就会给这个任务计时,5秒钟后Spring就会再次调用这个任务,但是发现原来的任务还在执行,这个时候第二个任务就阻塞了(这里只考虑单线程的情况下,多线程后面再讲),甚至如果第一个任务花费的时间过长,还可能会使第三第四个任务被阻塞,前一个任务执行完,后一个任务才能立马执行

cron和fixedRate一样默认单选程,都可能会阻塞。使用@Scheduled(fixedRate或者cron)时,如果任务需要频繁调度,或者任务非常耗时,则容易出现阻塞问题。 

4.initialDelayinitialDelayString

相同点:都表示延迟多久后再执行第一次任务,以后怎么执行则由其它规则(fixedDelay fixedRate )指定,可以与fixedDelay fixedRate搭配,不能与cron搭配。

区别点:后者支持占位符。

5.zone

时区,接收一个 java.util.TimeZone。表示会基于该时区解析。默认是一个空字符串,即取服务器所在地的时区。比如我们一般使用的时区Asia/Shanghai。该字段一般留空。

Timezone=Asia/Shanghai

二、之前的笔记

afd160c8b4bb43158dcd2f345aa64ccb.png

三、如何避免fixedRate任务阻塞

1.多线程注解

答案是加上注解@EnableAsync(类上)和@Async(方法上),加了注解(spring3.x之后才有这些注解)以后,就开启了多线程模式,当到了下一次任务的执行时机时,如果上一次任务还没执行完,就会自动创建一个新的线程来执行它,开启多线程后,每次任务开始的间隔都是一样的,可以理解为保证了任务以固定速度执行。但是还有缺陷

  • 第一是线程安全和事务问题。
  • 第二是这种情况下的线程是随着任务一执行完就销毁的,等下次有需要了程序会再创建一个线程,每次都重建明显影响性能,所以需要在代码里设置一个线程池。
    • 当未显式配置线程池时,Spring会自动使用SimpleAsyncTaskExecutor作为@Async的默认实现‌,核心特性:
      • 不提供任务队列(直接创建线程)‌和拒绝策略‌
      • 默认无并发限制(可通过setConcurrencyLimit()手动设置上限)‌
      • 每次调用都会创建新线程,不会复用已有线程‌(线程执行完即销毁)‌
      • 可以通过两种方式指定@Async的线程池替换默认的线程池,请看另一篇文章《Spring事件发布订阅、@Async指定线程池

2.配置Scheduling的线程池(implements SchedulingConfigurer)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

@Configuration
public class AppConfig implements SchedulingConfigurer {
  @Bean
  public Executor taskExecutor() {
    //指定定时任务线程数量,可根据需求自行调节
    return Executors.newScheduledThreadPool(3);
  }

  @Override
  public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
    scheduledTaskRegistrar.setScheduler(taskExecutor());
  }
}

现在程序就不会再自己创建线程了,每次都会从线程池里面拿。需注意的是,如果线程池里的所有线程都被拿去执行调度任务了,且又到了时间要执行一次任务,那么这个任务又会被阻塞。所以实际开发中如果想要保证任务以固定间隔被开始执行,线程池的最大线程数量可要想好,例如IO密集型任务的最大线程数一般大于CPU密集型任务的最大线程数。

四、cron

1.表达式语法

Cron表达式由6或7个时间字段组成:秒 分钟 小时 日期 月份 星期 年(可选);
字段       允许值      允许的特殊字符 
秒             0-59     , - * / 
分             0-59     , - * / 
小时            0-23     , - * / 
日期            1-31     , - * ? / L W C 
月份            1-12     , - * / 
星期            1-7       , - * ? / L C # 
年             1970-2099   , - * /
解析:
0/5 * * * * ? : 每5秒执行一次
“*”字符被用来指定所有的值。如:"*"在分钟的字段域里表示“每分钟”。 
“?”字符只在日期域和星期域中使用。它被用来指定“非明确的值”。当你需要通过在这两个域中的一个来指定一些东西的时候,它是有用的。看下面的例子你就会明白。 
月份中的日期和星期中的日期这两个元素时互斥的一起应该通过设置一个问号来表明不想设置那个字段。
“-”字符被用来指定一个范围。如:“10-12”在小时域意味着“10点、11点、12点”。
“,”字符被用来指定另外的值。如:“MON,WED,FRI”在星期域里表示”星期一、星期三、星期五”。
“/”字符用于指定增量。如:“0/15”在秒域意思是每分钟的0,15,30和45秒。“5/15”在分钟域表示每小时的5,20,35和50。 符号“*”在“/”前面(如:*/10)等价于0在“/”前面(如:0/10)。记住一条本质:表达式的每个数值域都是一个有最大值和最小值的集合,如: 秒域和分钟域的集合是0-59,日期域是1-31,月份域是1-12。字符“/”可以帮助你在每个字符域中取相应的数值。如:“7/6”在月份域的时候只 有当7月的时候才会触发,并不是表示每个6月。
L是‘last’的省略写法可以表示day-of-month和day-of-week域,但在两个字段中的意思不同,例如day-of- month域中表示一个月的最后一天。如果在day-of-week域表示‘7’或者‘SAT’,如果在day-of-week域中前面加上数字,它表示 一个月的最后几天,例如‘6L’就表示一个月的最后一个星期五。
字符“W”只允许日期域出现。这个字符用于指定日期的最近工作日。例如:如果你在日期域中写 “15W”,表示:这个月15号最近的工作日。所以,如果15号是周六,则任务会在14号触发。如果15好是周日,则任务会在周一也就是16号触发。如果 是在日期域填写“1W”即使1号是周六,那么任务也只会在下周一,也就是3号触发,“W”字符指定的最近工作日是不能够跨月份的。字符“W”只能配合一个 单独的数值使用,不能够是一个数字段,如:1-15W是错误的。
“L”和“W”可以在日期域中联合使用,LW表示这个月最后一周的工作日。
字符“#”只允许在星期域中出现。这个字符用于指定本月的某某天。例如:“6#3”表示本月第三周的星期五(6表示星期五,3表示第三周)。“2#1”表示本月第一周的星期一。“4#5”表示第五周的星期三。
字符“C”允许在日期域和星期域出现。这个字符依靠一个指定的“日历”。也就是说这个表达式的值依赖于相关的“日历”的计算结果,如果没有“日历” 关联,则等价于所有包含的“日历”。如:日期域是“5C”表示关联“日历”中第一天,或者这个月开始的第一天的后5天。星期域是“1C”表示关联“日历” 中第一天,或者星期的第一天的后1天,也就是周日的后一天(周一)。
例子如下:

0 59 23 * * ? 每天的23点59分执行

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 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发 
"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发

2.cron的函数式接口写法

AvailabilityManageSchedule.java:

import com.genertech.plm.aia.auth.service.AvailabilityManageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.util.StringUtils;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author: huo.qw
 * @createTime: 2024/09/09 下午 05:57
 * @description:
 */
@Configuration
@EnableScheduling
@Slf4j
public class AvailabilityManageSchedule implements SchedulingConfigurer {

  @Value("${plm.module.auth.cron}")
  private String crons;
  @Autowired
  private AvailabilityManageService availabilityManageService;

  /**
   * 执行定时任务.
   */
  @Override
  public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    taskRegistrar.addTriggerTask(
            //1.添加任务内容(Runnable)
            availabilityManageService::execute,
            //2.设置执行周期(Trigger)
            triggerContext -> {
              //2.1 从配置文件获取执行周期
              String cron = crons;
              //2.2 合法性校验.
              if (StringUtils.isEmpty(cron)) {
                log.warn("cron不能为空");
              }
              //2.3 返回执行周期(Date)
              Date date = new CronTrigger(cron).nextExecutionTime(triggerContext);
              log.info("----------定时任务下次执行时间:{}----------", this.dateToStr(date));
              return date;
            }
    );
  }

  public String dateToStr(Date date) {
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    return format.format(date);
  }
}

AvailabilityManageSchedule.java:

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.genertech.plm.aia.auth.entity.User;
import com.genertech.plm.aia.auth.jpa.repository.UserRepository;
import com.genertech.plm.aia.auth.openfeign.AvailabilityManageClient;
import com.genertech.plm.base.exception.ApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;


@Service
@Slf4j
public class AvailabilityManageService {

  @PostConstruct
  public void init(){
    log.info("项目启动时执行一次定时任务.");
    this.execute();
  }

  /**
   * 定时任务方法
   */
  public void execute() {
    log.info("----------开始执行本次定时任务----------");
    long startTime = System.currentTimeMillis();
    try {

      业务逻辑..
      
    } catch (ApiException e) {
      log.error("定时任务执行异常:", e);
    } finally {
      long endTime = System.currentTimeMillis();
      log.info("定时任务执行完毕用时{}ms", endTime - startTime);
    }
  }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值