为什么你要阻碍我,你看我扎不扎你「见鬼,定时任务延迟执行?」 - 第292篇

内心世界:你看我扎不扎你

悟纤:师傅,最近徒儿好扎心呐?

师傅:徒儿,这是谁扎你心了?

悟纤:最近碰到一个奇葩的问题,老是警告着我:信不信我扎你。

师傅:徒儿,请说人话。

悟纤:最近在研究定时任务的时候,发现明明设置的是每5秒执行一次,在测试环境跑的好好的,但是一到生产环境就不按套路出牌了。

师傅:徒儿,你这是碰到定时任务串行问题了。

悟纤:师傅,不会吧,真扎心了。

师傅:待师傅和你分析下。

一、定时任务回顾

       这里对于定时任务的步骤简单回顾下,更多资料可以查看之前的文章:

最全Spring Boot定时任务系列

Spring Scheduler定时任务使用步骤:

(1)启用对Spring Scheduler的支持:只需要在启动类或者配置类添加

@EnableScheduling

(2)定义一个Task类,在类上添加注解:@Component;

(3)创建定时任务:添加方法使用@Scheduled创建定时任务;

二、问题还原 (Spring Boot 2.1.9)

       这里我们创建一个Spring Boot项目还原下问题(对于Spring项目也是一样存在这样的问题),创建一个Task类:

@@Component
public class MyTask {

    //每10秒执行一次 
    @Scheduled(cron="0/10 * *  * * ? ")
    public void aTask(){
DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(Thread.currentThread().getName()+":" +sdf.format(new Date())+" --> A任务每10秒执行一次");
        try {
            TimeUnit.SECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //每5秒执行一次 
    @Scheduled(cron="0/5 * *  * * ? ")
    public void bTask(){
        DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(Thread.currentThread().getName()+":" +sdf.format(new Date())+" --> B任务每5秒执行一次");
    }
}

BTW

(1)在这个定时任务中有两个任务aTask和bTask。

(2)aTask是每10s执行一次,bTask是每5s执行一次。

(3)aTask在方法中使用sleep(20),那么这个方法就需要执行20s才能执行完成。

那么这时候问题来了?

师傅:徒儿,假设15:31:10 (15点31分10秒) 开始执行aTask(不考虑bTask),那么aTask的下一次执行时间是?

悟纤:(思索中:肯定不能是15:31:20了,不然师傅也不会问我,10s之后,延迟20s,该不会是15:31:30吧)师傅,我知道了,是15:31:30。

师傅:恭喜徒儿,你掉进坑里了,这里的时间是很有学问的哦。

悟纤:怎么说?

师傅:你看10s开始执行,方法中延迟了20s,那么时间就到了30s,此时方法并还没执行完毕,假设执行了1毫秒。我们的定时任务是每10秒执行一次,那么第二次执行的时间本应该是20s,但是无奈方法sleep了,所以到30s,此时也应该执行的,但是方法sleep之后,还需要在耗时一小部分时间,又不能执行,到了40s之后,此时方法都执行完毕了,再次进入定时方法进行执行。

悟纤:所以第二次执行的时间是:15:31:40?

师傅:是的。这个稍一不留神就会掉进坑里了。

       我们看下控制台的打印(测试的时候,记得把bTask先注释掉):

scheduling-1:2019-10-1515:31:10 --> A任务每10秒执行一次

scheduling-1:2019-10-1515:31:40 --> A任务每10秒执行一次

scheduling-1:2019-10-1515:32:10 --> A任务每10秒执行一次

       所以结果就是虽然sleep了20s,但是每次执行的间隔确是30s。

师傅:如果将sleep 20s,调整为19s的话,那么第二次的执行时间呢?

悟纤:这次就应该是15:31:30了。

师傅:如果aTask,bTask都执行定时的话,那么假设bTask在10:43:40开始执行第一次,第二次执行的时间是?

悟纤:这个理论上来说aTask和bTask两个定时任务之间应该互相不影响的,bTask每5s执行一次,那么下次执行的时间就是10:43:45。

师傅:这个就有话说了,比较复杂,要是能够按照你说的理论情况,那么为师也没有进行讲解了。

     

  当bTask先执行的话,看下控制台打印(aTask sleep 20s):

cheduling-1:2019-10-1610:43:40 --> B任务每5秒执行一次

scheduling-1:2019-10-1610:43:40 --> A任务每10秒执行一次

scheduling-1:2019-10-1610:44:00 --> B任务每5秒执行一次

scheduling-1:2019-10-1610:44:05 --> B任务每5秒执行一次

scheduling-1:2019-10-1610:44:10 --> B任务每5秒执行一次

scheduling-1:2019-10-1610:44:10 --> A任务每10秒执行一次

当bTask在43:40完第一次之后,A任务也开始执行,B任务是在20s之后执行了。

       当aTask先执行的时候,看下控制台打印:

scheduling-1:2019-10-1611:03:20 --> A任务每10秒执行一次

scheduling-1:2019-10-1611:03:40 --> B任务每5秒执行一次

scheduling-1:2019-10-1611:03:45 --> B任务每5秒执行一次

scheduling-1:2019-10-1611:03:50 --> B任务每5秒执行一次

scheduling-1:2019-10-1611:03:50 --> A任务每10秒执行一次

scheduling-1:2019-10-1611:04:10 --> B任务每5秒执行一次

scheduling-1:2019-10-1611:04:15 --> B任务每5秒执行一次

scheduling-1:2019-10-1611:04:20 --> B任务每5秒执行一次

scheduling-1:2019-10-1611:04:20 --> A任务每10秒执行一次

       当aTask执行的时候,在03:20执行完毕,sleeple 20s,此时B任务在这区间并没有执行,在aTask sleep 20s之后,开始了第一次执行03:40,过了5s执行了第二次,之后的都会由于aTask的sleep 20s而影响了bTask的每5s执行过滤。

       上面说了这么多废话,其实就是一句话:你阻碍了我的执行了(aTask阻碍了bTask的执行)。

       那么为什么要造成如此的现象呐?

       真相只有一个,那就是:Spring的定时任务默认是单线程。

Spring多个任务执行起来时间会有问题(B任务会因为A任务执行起来需要20S而被延后20S执行)。

三、问题解决

悟纤:原来是这么回事呐,那都有什么方案可以解决呢?

师傅:方案多多。

实现方式:

(1)异步执行:使用@EnableAsync @Async定时任务异步执行。

(2)多线程执行定时任务:可以使用ScheduledTaskRegistrar注入线程池或者注入TaskScheduler。

3.1 异步执行

       异步执行主要是在定时任务方法上添加@Async,主要有两个步骤:
(1)在启动类上添加@EnableAsync的注解,启用异步任务。

(2)在方法上添加@Async:

@Component
public class MyTask {

    //每10秒执行一次 
    @Scheduled(cron="0/10 * *  * * ? ")
    @Async
    public void aTask(){
        DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(Thread.currentThread().getName()+":" +sdf.format(new Date())+" --> A任务每10秒执行一次");
        try {
            TimeUnit.SECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //每5秒执行一次 
    @Scheduled(cron="0/5 * *  * * ? ")
    @Async
    public void bTask(){
        DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(Thread.currentThread().getName()+":" +sdf.format(new Date())+" --> B任务每5秒执行一次");
    }
}

       这时候启动查看控制台的打印信息:

task-2:2019-10-1615:02:00 --> A任务每10秒执行一次

task-1:2019-10-1615:02:00 --> B任务每5秒执行一次

task-3:2019-10-1615:02:05 --> B任务每5秒执行一次

task-4:2019-10-1615:02:10 --> B任务每5秒执行一次

task-5:2019-10-1615:02:10 --> A任务每10秒执行一次

task-6:2019-10-16 15:02:15--> B任务每5秒执行一次

此时A和B任务互相不影响,而且A的sleep也不影响A的任务了。

BTW:注意上面的线程名称,使用的是不一样的。

3.2 多线程执行定时任务

方式一:使用ScheduledTaskRegistrar配置线程池

       这种方式实现SchedulingConfigurer的configureTasks的方法:

@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        //设定一个长度10的定时任务线程池
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
    }
}

       执行程序,查看控制台的信息打印:

pool-1-thread-2:2019-10-1615:08:20 --> B任务每5秒执行一次

pool-1-thread-1:2019-10-1615:08:20 --> A任务每10秒执行一次

pool-1-thread-3:2019-10-1615:08:25 --> B任务每5秒执行一次

pool-1-thread-2:2019-10-1615:08:30 --> B任务每5秒执行一次

pool-1-thread-4:2019-10-1615:08:35 --> B任务每5秒执行一次

pool-1-thread-3:2019-10-1615:08:40 --> B任务每5秒执行一次

pool-1-thread-5:2019-10-1615:08:45 --> B任务每5秒执行一次

pool-1-thread-2:2019-10-1615:08:50 --> A任务每10秒执行一次

pool-1-thread-6:2019-10-1615:08:50 --> B任务每5秒执行一次

       查看上面的信息,不难发现A和B的任务在不同的线程中执行,所以互相不影响,A的sleep会影响到下一次执行。

方式二:注入TaskScheduler

       这种方式主要点是使用ThreadPoolTaskScheduler进行管理定时任务:

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);
        return scheduler;
    }    

       执行结果:

taskScheduler-1:2019-10-1615:19:20 --> B任务每5秒执行一次

taskScheduler-2:2019-10-1615:19:20 --> A任务每10秒执行一次

taskScheduler-3:2019-10-1615:19:25 --> B任务每5秒执行一次

taskScheduler-6:2019-10-1615:19:55 --> B任务每5秒执行一次

       执行结果和上面的方式是一样的,区别的地方也是线程的名称不一样了。

四、题外话

4.1 @EnableScheduling注解的作用

@EnableScheduling注解,它的作用是发现注解 @Scheduled的任务并由后台执行。没有它的话将无法执行定时任务。

4.2 Spring项目中如何解决定时任务延迟问题呐?

悟纤:上面我们讲的是SpringBoot项目中如何解决,在实际中,还有很多项目是使用Spring的项目的,那么在Spring中如何解决呢?

师傅:徒儿这个问题提的好。对于Spring的话,主要是通过配置文件进行配置线程池的个数来实现。可以参考如下:

如果使用的注解方式主要是添加如下配置:

    <task:scheduler id="scheduler" pool-size="10"/>
    <task:executor id="executor"  pool-size="5"/>
    <task:annotation-driven scheduler="scheduler" executor="executor"/>

       如果使用的是配置文件的方式,那么在上面的基础上在添加如下配置:

<task:scheduled-tasks scheduler="scheduler" >
        <!--↓↓ 每周五23时:检查是否已经太久没有登陆了. ↓↓ --> 
          <task:scheduled ref="basetask" method="checkIsLongNoLogin" cron="0 0 23 ? * 6" />     
</task:scheduled-tasks>

师傅:徒儿这种方式在SpringBoot中是否可以使用呢?留作课后思考题。

4.3 定时任务表达式是否可以配置到配置文件

       可以的,在application.properties文件中添加:

job.cron.bTask = 0/5 * *  * * ?

       在代码中引入使用即可:

@Scheduled(cron="${job.cron.bTask}")
public void bTask(){}

五、悟纤小结

师傅:今天就学习这么多了,徒儿,来,剩下的时间,你给大家来个总结吧:

(1)引起定时任务延迟原因:Spring/Spring Boot的定时任务默认是单线程 (敲重点)。

(2)解决定时任务延迟问题:异步任务多线程(敲重点)。

(3)异步任务定时任务执行情况:A/B任务互相不影响,并且A的Sleep也不会影响A的下次执行。

(4)多线程定时任务执行情况:A/B任务互相不影响,A的Sleep会影响到A的下次执行。

我就是我,是颜色不一样的烟火。
我就是我,是与众不同的小苹果。

à悟空学院:https://t.cn/Rg3fKJD

学院中有Spring Boot相关的课程!

SpringBoot视频:https://t.cn/R3QepWG

Spring Cloud视频:https://t.cn/R3QeRZc

SpringBoot Shiro视频:https://t.cn/R3QDMbh

SpringBoot交流平台:https://t.cn/R3QDhU0

SpringData和JPA视频:https://t.cn/R1pSojf

SpringSecurity5.0视频:https://t.cn/EwlLjHh

Sharding-JDBC分库分表实战:https://t.cn/E4lpD6e

分布式事务解决方案「手写代码」:http://t.cn/AieNUirK

发布了159 篇原创文章 · 获赞 200 · 访问量 68万+

猜你喜欢

转载自blog.csdn.net/linxingliang/article/details/103727013