Spring定时任务@Scheduled的使用

  • Post author:
  • Post category:其他




说明

@Scheduled是spring自带的注解,默认是

单线程

,常用作定时任务使用,

但是如果是

集群版

的机器的话,就考虑加上 分布式锁 或者使用 分布式定时任务(Elasticjob或者xxl-job等)代替。



简单的使用

1、在启动类上添加注解

@EnableScheduling

@SpringBootApplication
@EnableScheduling
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

2、在一个Bean中(

@Component

,需要将写定时任务的类交给spring管理)编写一个public void无参数方法,然后加上注解

@Scheduled

@Component
public class OrderJob {

//    @Scheduled(fixedDelay = 2000) //固定延迟:代表下一个任务的开始与上一个任务的结束间隔总是固定的时长(毫秒),而且总是会等上一个任务完成了,才会开启下一个任务
//    @Scheduled(fixedRate = 2000) //固定频率:定频任务的特性是任务的执行的时间间隔总是一样的。比如每1000毫秒执行一次
//    @Scheduled(initialDelay = 10_000, fixedRate = 2_000) //启动延迟10秒,并以2秒的间隔执行任务
    @Scheduled(cron = "*/2 * * * * ?") //使用cron表达式:每隔2秒执行一次任务
    public void checkOrder() throws InterruptedException {
        System.out.println("====================start" + LocalTime.now());
        Thread.sleep(1000);
        System.out.println("====================end" + LocalTime.now());
        System.out.println();
    }
}



一些常用的cron表达式

每隔5秒执行一次:*/5 * * * * ?
每隔10分钟执行一次:0 */10 * * * ?
每天23点执行一次:0 0 23 * * ?
每天凌晨1点执行一次:0 0 1 * * ?
每月1号凌晨1点执行一次:0 0 1 1 * ?
每月最后一天23点执行一次:0 0 23 L * ?
每周星期天凌晨1点实行一次:0 0 1 ? * L26分、29分、33分执行一次:0 26,29,33 * * * ?
每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?



其他参数设置

默认使用服务器默认时区
	可以设置为java.util.TimeZone中的zoneId, 每天零点执行@Scheduled(cron = "0 0 0 * * ?", zone = "Asia/Shanghai")

fixedDelay 固定延迟
	@Scheduled(fixedDelay = 5000) //上一次执行完毕时间点之后5秒再执行

fixedDelayString
	与 fixedDelay 意思相同,只是使用字符串的形式,唯一不同的是支持占位符
	@Scheduled(fixedDelayString = "5000")

fixedRate 固定频率
	@Scheduled(fixedRate = 5000) //上一次开始执行时间点之后5秒再执行(每隔5秒执行一次)
	---但是要注意:如果任务的执行所需时间(比如10秒)超过了 fixedRate(5秒),
	---那么会等该次任务结束后才会执行下一次任务(单线程 串行),即相当于每隔10秒执行一次了

fixedRateString
	与 fixedRate 意思相同,只是使用字符串的形式,唯一不同的是支持占位符

initialDelay
	@Scheduled(initialDelay=1000, fixedRate=5000) //第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次

initialDelayString
	initialDelay 的字符串String格式,initialDelay 意思相同,只是使用字符串的形式,唯一不同的是支持占位符



可以把定时任务的配置放到配置文件中,根据实际情况要修改时,不需要重新编译代码(需重启服务)

@Component
public class OrderJob {
    
    //ps:如果在配置文件中未读取task.cron,那么会以下面提供的表达式默认执行
    @Scheduled(cron = "${task.cron:*/2 * * * * ?}") //每隔2秒执行一次任务
    public void checkOrder(){
        System.out.println("====================" + LocalDateTime.now());
    }
}



优缺点

优点

简单,拆箱即用,一些单机任务的情况比较适合

缺点

单线程:如果有两个任务A和B,那么任务A要是阻塞了,任务B就无法执行。

不支持集群:为避免重复执行的问题

不支持生命周期统一管理:不重启服务情况下关闭,启动任务

不支持动态调整:不重启服务的情况下修改任务参数

不支持分片任务:处理有序数据时,多机器分片执行任务处理不同数据

无报警机制:任务失败之后没有报警机制

不支持失败重试:出现异常后任务中介,不能根据执行状态控制任务重新执行

任务数据统计难以统计:任务数据量大时,对于任务执行情况无法高效的统计执行情况



使用多线程

因为@Scheduled默认使用的是单线程,如果有两个任务A和B,那么任务A要是阻塞了,任务B就无法执行,如何解决?

需要实现SchedulingConfigurer接口,然后自定义线程池,这样凡是用到@Scheduled注解的都可以用该线程池

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

import java.util.concurrent.Executors;

/**
 * 多线程执行定时任务
 */
@Configuration
//所有的定时任务都放在一个线程池中,定时任务启动时使用不同都线程。
public class ScheduleConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        //设定一个长度10的定时任务线程池
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
    }
}

可以通过Thread.currentThread().getName()拿到线程名称,便于在日志中进行排查问题。



补充:动态修改定时规则

import gov.minhang.aqrcode.basic.utils.DateUtils;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
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.stereotype.Component;

import java.time.LocalTime;
import java.util.Date;

/**
 * 动态修改定时任务cron参数
 */
@Component
@EnableScheduling
public class TestTask implements SchedulingConfigurer {

    private static String cron = "*/2 * * * * ?";

    /**
     * 提供set方法,由外部根据不同条件而修改cron表达式的值;
     */
    public static void setCron(String param) {
        cron = param;
    }

    public static String getCron() {
        return cron;
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(
                new Runnable() {
                    @Override
                    public void run() {
                        // 这里执行定时任务的业务逻辑
                        System.out.println(Thread.currentThread().getName() + " " + LocalTime.now() + " 定时任务的业务逻辑 ");
                    }
                }, new Trigger() {
                    @Override
                    public Date nextExecutionTime(TriggerContext triggerContext) {
                        // 定时任务触发,可修改定时任务的执行周期
                        CronTrigger trigger = new CronTrigger(cron);
                        Date nextExecDate = trigger.nextExecutionTime(triggerContext);
                        System.out.println(Thread.currentThread().getName() + " " + LocalTime.now() + " 修改任务的执行周期 " + " cron: " + cron + " 下次执行时间:" + DateUtils.date2LocalDateTime(nextExecDate));
                        return nextExecDate;
                    }
                }
        );
    }
}

controller中方法:

    /**
     * 修改定时任务的cron
     */
    @GetMapping({"/updateTaskCron"})
    public R<Boolean> updateTaskCron(@RequestParam String cron) {
        TestTask.setCron(cron);
        return R.success();
    }

    /**
     * 查询定时任务的cron
     */
    @GetMapping({"/queryTaskCron"})
    public R<String> queryTaskCron() {
        return R.success(TestTask.getCron());
    }



版权声明:本文为W1324926684原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。