zoukankan      html  css  js  c++  java
  • 浅析SpringBoot中使用@scheduled定时执行任务需要注意的单线程的坑

      SpringBoot使用@scheduled定时执行任务的时候是在一个单线程中,如果有多个任务,其中一个任务执行时间过长,则有可能会导致其他后续任务被阻塞直到该任务执行完成,也就是会造成一些任务无法定时执行的错觉。无论@scheduled是用在一个类的多个方法还是用在多个类中的方法,默认都是单线程的。(其描述和测试可以看这篇博客:https://blog.csdn.net/zmemorys/article/details/105201647)

    一、问题现象

      可以通过如下代码进行测试:

        @Scheduled(cron = "0/1 * * * * ? ")
        public void deleteFile() throws InterruptedException {
            log.info("111delete success, time:" + new Date().toString());
            Thread.sleep(1000 * 5);//模拟长时间执行,比如IO操作,http请求
        }
    
        @Scheduled(cron = "0/1 * * * * ? ")
        public void syncFile() {
            log.info("222sync success, time:" + new Date().toString());
        }
        
    /**输出如下:
    [pool-1-thread-1] : 111delete success, time:Mon Nov 26 20:42:13 CST 2018
    [pool-1-thread-1] : 222sync success, time:Mon Nov 26 20:42:18 CST 2018
    [pool-1-thread-1] : 111delete success, time:Mon Nov 26 20:42:19 CST 2018
    [pool-1-thread-1] : 222sync success, time:Mon Nov 26 20:42:24 CST 2018
    [pool-1-thread-1] : 222sync success, time:Mon Nov 26 20:42:25 CST 2018
    [pool-1-thread-1] : 111delete success, time:Mon Nov 26 20:42:25 CST 2018
    上面的日志中可以明显的看到syncFile被阻塞了,直达deleteFile执行完它才执行了
    而且从日志信息中也可以看出@Scheduled是使用了一个线程池中的一个单线程来执行所有任务的。
    **/
    
    /**如果把Thread.sleep(1000*5)注释了,输出如下:
    [pool-1-thread-1]: 111delete success, time:Mon Nov 26 20:48:04 CST 2018
    [pool-1-thread-1]: 222sync success, time:Mon Nov 26 20:48:04 CST 2018
    [pool-1-thread-1]: 222sync success, time:Mon Nov 26 20:48:05 CST 2018
    [pool-1-thread-1]: 111delete success, time:Mon Nov 26 20:48:05 CST 2018
    [pool-1-thread-1]: 111delete success, time:Mon Nov 26 20:48:06 CST 2018
    [pool-1-thread-1]: 222sync success, time:Mon Nov 26 20:48:06 CST 2018
    这下正常了
    **/

    二、解决方案

    1、将@Scheduled注释的方法内部改成异步执行

      //当然:构建一个合理的线程池也是一个关键,否则提交的任务也会在自己构建的线程池中阻塞
        ExecutorService service = Executors.newFixedThreadPool(5);
    
        @Scheduled(cron = "0/1 * * * * ? ")
        public void deleteFile() {
            service.execute(() -> {
                log.info("111delete success, time:" + new Date().toString());
                try {
                    Thread.sleep(1000 * 5);//改成异步执行后,就算你再耗时也不会印象到后续任务的定时调度了
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    
        @Scheduled(cron = "0/1 * * * * ? ")
        public void syncFile() {
            service.execute(()->{
                log.info("222sync success, time:" + new Date().toString());
            });
        }

    2、把Scheduled配置成成多线程执行

    @Configuration
    public class ScheduleConfig implements SchedulingConfigurer {
        @Override
        public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
            //当然了,这里设置的线程池是corePoolSize也是很关键了,自己根据业务需求设定
            taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
    /**为什么这么说呢? 假设你有4个任务需要每隔1秒执行,而其中三个都是比较耗时的操作可能需要10多秒,而你上面的语句是这样写的: taskRegistrar.setScheduler(Executors.newScheduledThreadPool(3)); 那么仍然可能导致最后一个任务被阻塞不能定时执行 **/ } }

      关于具体如何配置多线程执行,可以具体下面这篇博客文章:

    三、如何配置多线程执行

    1、问题背景

      公司在使用定时任务的时候,使用的是spring scheduled。代码如下:

    @EnableScheduling
    public class TaskFileScheduleService {
        @Scheduled(cron="0 */1 * * * ?")
        public void task1(){
        .......
        }
        @Scheduled(cron="0 */1 * * * ?")
        public void task2(){
        .......
        }
    }

      某天生产环境的定时任务不跑了,赶紧给看看~线程卡死这种问题,第一步当然是将jvm中的heap dump和thread dump导出来~经过简单分析,thread dump中某个线程确实一直处理running状态,heap dump没啥问题~

      thread dump中的问题线程:

    "pool-2-thread-43" #368 prio=5 os_prio=0 tid=0x00005587fd54c800 nid=0x1df runnable [0x00007ff7e2056000]
       java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:171)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at java.net.SocketInputStream.read(SocketInputStream.java:224)
        at ch.ethz.ssh2.transport.ClientServerHello.readLineRN(ClientServerHello.java:30)
        at ch.ethz.ssh2.transport.ClientServerHello.<init>(ClientServerHello.java:67)
        at ch.ethz.ssh2.transport.TransportManager.initialize(TransportManager.java:455)
        at ch.ethz.ssh2.Connection.connect(Connection.java:643)
        - locked <0x000000074539e0e8> (a ch.ethz.ssh2.Connection)
        at ch.ethz.ssh2.Connection.connect(Connection.java:490)
        - locked <0x000000074539e0e8> (a ch.ethz.ssh2.Connection)
        at com.suneee.yige.medicalserver.common.SSHUtils.connect(SSHUtils.java:24)
        at com.suneee.yige.medicalserver.service.TaskFileScheduleService.getConn(TaskFileScheduleService.java:102)
        at com.suneee.yige.medicalserver.service.TaskFileScheduleService.taskInfo(TaskFileScheduleService.java:108)
        at com.suneee.yige.medicalserver.service.TaskFileScheduleService.task(TaskFileScheduleService.java:74)
        at sun.reflect.GeneratedMethodAccessor295.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:65)
        at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
        at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:81)
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

      很明显,ch.ethz.ssh2.Connection.connect这个方法卡死,导致线程一直处于running状态。

      由于 spring scheduled 默认是所有定时任务都在一个线程中执行!!这是个大坑!!!也就是说定时任务1一直在执行,定时任务2一直在等待定时任务1执行完成。这就导致了生产上定时任务全部卡死的现象。

      问题已经很明确了,要么解决ch.ethz.ssh2.Connection.connect卡死的问题,要么解决spring scheduled单线程处理的问题。

      首先,想到的是处理ch.ethz.ssh2.Connection.connect卡死的问题,但是经过一番查找,发现这个ssh的工具包很久没更更新过了,也没有设置例如httpclient的超时时间之类的。这就很难办了!果断放弃!!现在只剩一条路,怎么在任务1卡死的时候,任务2可以按他自己的周期执行,且任务1也按照固定周期执行,不会因为某次任务1卡死导致后续的定时任务出现问题!

    2、解决思路

    (1)方法一:添加配置

    @Configuration
    public class ScheduleConfig implements SchedulingConfigurer {
        @Override
        public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
            taskRegistrar.setScheduler(Executors.newScheduledThreadPool(50));
        }
    }

      这个方法,在程序启动后,会逐步启动50个线程,放在线程池中。每个定时任务会占用1个线程。但是相同的定时任务,执行的时候,还是在同一个线程中。

      例如,程序启动,每个定时任务占用一个线程。任务1开始执行,任务2也开始执行。如果任务1卡死了,那么下个周期,任务1还是处理卡死状态,任务2可以正常执行。也就是说,任务1某一次卡死了,不会影响其他线程,但是他自己本身这个定时任务会一直等待上一次任务执行完成!

    (2)方法二:添加配置

    @Configuration
    @EnableAsync
    public class ScheduleConfig {
    
        @Bean
        public TaskScheduler taskScheduler() {
            ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
            taskScheduler.setPoolSize(50);
            return taskScheduler;
        }
    }

      在方法上添加注解@Async

    @EnableScheduling
    public class TaskFileScheduleService {
        @Async
        @Scheduled(cron="0 */1 * * * ?")
        public void task1(){
        .......
        }
        @Async
        @Scheduled(cron="0 */1 * * * ?")
        public void task2(){
        .......
        }
    }

      这种方法,每次定时任务启动的时候,都会创建一个单独的线程来处理。也就是说同一个定时任务也会启动多个线程处理。例如:任务1和任务2一起处理,但是线程1卡死了,任务2是可以正常执行的。且下个周期,任务1还是会正常执行,不会因为上一次卡死了,影响任务1。但是任务1中的卡死线程越来越多,会导致50个线程池占满,还是会影响到定时任务。这时候,可能会几个月发生一次~到时候再重启就行了!

      至于这 2 种方案怎么选择,可以根据自己业务去定。

    参考文章:

    SpringBoot中使用@scheduled定时执行任务需要注意的坑  —— https://blog.csdn.net/zhaominpro/article/details/84561966

    spring scheduled单线程和多线程使用过程中的大坑 —— https://blog.csdn.net/FlyingSnails/article/details/90167434

  • 相关阅读:
    asp.net[web.config] httphandlers 与实现自由定义访问地址
    Web.config配置文件详解
    ASP.NET十分有用的页面间传值方法(转)
    web.config中authorization下的location中的path的设置 (转)
    基于FormsAuthentication的用户、角色身份认证(转)
    FormsAuthentication.RedirectFromLoginPage 登录
    php代码审计-下载站系统Simple Down v5.5.1 xss跨站漏洞分析
    创建二维码生成器
    更新自制入库价格(结账)
    常用的系统存储过程
  • 原文地址:https://www.cnblogs.com/goloving/p/15065200.html
Copyright © 2011-2022 走看看