@Scheduled定时任务

本文介绍了如何在SpringBoot项目中使用@Scheduled注解创建定时任务,强调了单线程执行导致的任务顺序问题,并探讨了如何通过异步执行和自定义线程池来优化任务调度。最后,提出使用接口和数据库存储Cron表达式以实现实时更新。
摘要由CSDN通过智能技术生成

在SpringBoot项目中使用定时任务时可以使用@Scheduled标注在需要定时执行的方法上,@Scheduled注解要生效还需要在系统启动类或配置类上添加@EnableScheduling注解。

@Scheduled属性:

1、启用Scheduling

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

2、创建任务调度类加入Spring容器中以及具体的任务

创建定时任务调度类TestSchedule,并定义taskSchedule1方法使用@Scheduled(cron = "0/10 * * * * ?")标注,表示该方法从0秒开始,每隔10秒执行一次,方法内部获取了执行当前任务的线程,打印了任务开始和结束的时间、线程ID,sleep 5秒表示任务执行完成需要花费5秒时间。

@Component
public class TestSchedule {
    @Scheduled(cron = "0/10 * * * * ?")
    public void taskSchdule1() throws InterruptedException {
        Thread t = Thread.currentThread();
        System.out.println("taskSchule1 "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
        Thread.sleep(5000);
        System.out.println("taskSchule1 end "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
    }
}

该任务执行结果:

taskSchdule1每隔10秒执行一次,并且每次执行5秒。

在TestSchedule类中添加第二个需要任务调度方法,每隔3秒执行一次。

@Scheduled(cron = "0/3 * * * * ?")
public void taskSchdule2(){
    Thread t = Thread.currentThread();
    System.out.println("taskSchule2 "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+  t.getId());
}

当前执行结果如下:

发现问题:taskSchule2,在a、b两处时间相差7秒已超过3秒,则taskSchule2是在taskSchule1执行结束才立即执行的,推测多个任务是串行的。

Spring中@EnableScheduling和@Scheduled标注的定时任务默认是单线程执行的,这里taskSchule1执行任务需要花费较长时间,所有阻塞了taskSchule2的执行。

解决方法:使用@Async和@EnableAsync异步执行任务,@Async 注解用于标记一个方法是异步方法,即该方法可以在独立的线程中执行,而不会阻塞当前线程。被 @Async 注解标记的方法通常需要在类上添加 @EnableAsync 注解来启用异步方法执行的支持。

修改启动类:

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

修改任务调度类:

@Component
@Async
public class TestSchedule {
    @Scheduled(cron = "0/10 * * * * ?")
    public void taskSchdule1() throws InterruptedException {
        Thread t = Thread.currentThread();
        System.out.println("taskSchule1 "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
        Thread.sleep(5000);
        System.out.println("taskSchule1 end "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
    }

    @Scheduled(cron = "0/3 * * * * ?")
    public void taskSchdule2(){
        Thread t = Thread.currentThread();
        System.out.println("taskSchule2 "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+  t.getId());
    }

}

在TestSchedule类上标注@Async注解,表示该类中所有标注了@Scheduled的方法都使用异步处理方式。

修改后执行结果:taskSchdule1每次间隔10秒执行,taskSchdule2每次间隔3秒执行

再次稍作修改:将任务的睡眠时间改成11秒 Thread.sleep(11000);,此时任务1的执行时间已经超过了它的调度时间。再次运行程序结果如下:

发现taskSchdule1交叉执行,两组是不同线程执行的,因为taskSchdule1的执行时间超过了调度时间,所以,a处taskSchdule1.task-3开始执行,在未执行完成的情况下,任务的调度时间到了,其他线程有立马调度了任务从c处开始执行。

综上解决两种问题方法:

去掉启动类中的@EnableAsync和@EnableScheduling注解,去掉调度类中的@Async注解

//启动类
@SpringBootApplication
public class SpringbootApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootApplication.class, args);
    }
}


//调度任务类
@Component
public class TestSchedule {
    @Scheduled(cron = "0/10 * * * * ?")
    public void taskSchdule1() throws InterruptedException {
        Thread t = Thread.currentThread();
        System.out.println("taskSchule1 "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
        Thread.sleep(11000);
        System.out.println("taskSchule1 end "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+ t.getId());
    }

    @Scheduled(cron = "0/3 * * * * ?")
    public void taskSchdule2(){
        Thread t = Thread.currentThread();
        System.out.println("taskSchule2 "+ DateTimeUtils.dateToTimeStr(new Date()) +" ThreadID:"+  t.getId());
    }

}

创建一个任务配置类ScheduleConfig 实现SchedulingConfigurer接口的configureTasks方法,使用参数taskRegistrar为任务调度创建线程池。

@Configuration
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {

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

    @Bean(destroyMethod="shutdown")
    public Executor taskExecutor() {
        return Executors.newScheduledThreadPool(10);
    }
}

此时的执行结果:

现在taskSchdule1和taskSchdule2均运行正常,并且taskSchdule1不会出现交叉现象, taskSchdule1第二次调度会等到第一次调度执行完毕后的下一个调度时间点才会执行。

3、优化

如果我们修改了cron表达式,需要重启整个应用才能生效,不是很方便。想要实现修改cron表达式就生效就需要用到接口的方式来实现定时任务,并将cron表达式定义在数据库中,修改数据库中对应的cron表达式就可以实时生效。

1、首先定义一张表用来记录cron表达式

CREATE TABLE `scheduled` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NULL,
  `cron` varchar(255) NULL,
  PRIMARY KEY (`id`)
)
INSERT INTO `scheduled` (`id`, `name`, `cron`) VALUES (1, '定时任务1', '0/6 * * * * ?')

2、实现定时任务


@Component
@EnableScheduling //开启定时任务
public class ScheduleTask implements SchedulingConfigurer {
    @Autowired
    private CronMapper cronMapper;
 
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(
                //添加任务内容
                () -> process(),
                //设置执行的周期
                triggerContext -> {
                    //查询cron表达式
                    String cron = cronMapper.getCron(1L);
                    if (cron.isEmpty()) {
                        System.out.println("cron is null");
                    }
                    return new CronTrigger(cron).nextExecutionTime(triggerContext);
                });
    }
 
    private void process() {
        System.out.println("基于接口的定时任务-具体业务逻辑");
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值