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