Spring Boot使用@Async實現異步調用:ThreadPoolTaskScheduler線程池的優雅關閉

最近發了一篇關於Spring Boot中使用 @Async來實現異步任務和線程池控制的文章:《Spring Boot使用@Async實現異步調用:自定義線程池》。由於最近身邊也發現了不少異步任務沒有正確處理而導致的問題,所以本文就接前面的內容,繼續說說線程池的優雅關閉,主要針對 ThreadPoolTaskScheduler線程池。

問題現象

在上篇文章的例子中,我們定義了一個線程池,然後利用 @Async註解寫了3個任務,並指定了這些任務執行使用的線程池。在上文的單元測試中,我們沒有具體說說shutdown相關的問題,下面我們就來模擬一個問題現場出來。

第一步:如前文一樣,我們定義一個 ThreadPoolTaskScheduler線程池:


 
  1. @SpringBootApplication

  2. public class Application {

  3.  

  4.    public static void main(String[] args) {

  5.        SpringApplication.run(Application.class, args);

  6.    }

  7.  

  8.    @EnableAsync

  9.    @Configuration

  10.    class TaskPoolConfig {

  11.  

  12.        @Bean("taskExecutor")

  13.        public Executor taskExecutor() {

  14.            ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();

  15.            executor.setPoolSize(20);

  16.            executor.setThreadNamePrefix("taskExecutor-");

  17.            return executor;

  18.        }

  19.  

  20.    }

  21.  

  22. }

第二步:改造之前的異步任務,讓它依賴一個外部資源,比如:Redis


 
  1. @Slf4j

  2. @Component

  3. public class Task {

  4.  

  5.    @Autowired

  6.    private StringRedisTemplate stringRedisTemplate;

  7.  

  8.    @Async("taskExecutor")

  9.    public void doTaskOne() throws Exception {

  10.        log.info("開始做任務一");

  11.        long start = System.currentTimeMillis();

  12.        log.info(stringRedisTemplate.randomKey());

  13.        long end = System.currentTimeMillis();

  14.        log.info("完成任務一,耗時:" + (end - start) + "毫秒");

  15.    }

  16.  

  17.    @Async("taskExecutor")

  18.    public void doTaskTwo() throws Exception {

  19.        log.info("開始做任務二");

  20.        long start = System.currentTimeMillis();

  21.        log.info(stringRedisTemplate.randomKey());

  22.        long end = System.currentTimeMillis();

  23.        log.info("完成任務二,耗時:" + (end - start) + "毫秒");

  24.    }

  25.  

  26.    @Async("taskExecutor")

  27.    public void doTaskThree() throws Exception {

  28.        log.info("開始做任務三");

  29.        long start = System.currentTimeMillis();

  30.        log.info(stringRedisTemplate.randomKey());

  31.        long end = System.currentTimeMillis();

  32.        log.info("完成任務三,耗時:" + (end - start) + "毫秒");

  33.    }

  34.  

  35. }

注意:這裏省略了pom.xml中引入依賴和配置redis的步驟

第三步:修改單元測試,模擬高併發情況下ShutDown的情況:


 
  1. @RunWith(SpringJUnit4ClassRunner.class)

  2. @SpringBootTest

  3. public class ApplicationTests {

  4.  

  5.    @Autowired

  6.    private Task task;

  7.  

  8.    @Test

  9.    @SneakyThrows

  10.    public void test() {

  11.  

  12.        for (int i = 0; i < 10000; i++) {

  13.            task.doTaskOne();

  14.            task.doTaskTwo();

  15.            task.doTaskThree();

  16.  

  17.            if (i == 9999) {

  18.                System.exit(0);

  19.            }

  20.        }

  21.    }

  22.  

  23. }

說明:通過for循環往上面定義的線程池中提交任務,由於是異步執行,在執行過程中,利用 System.exit(0)來關閉程序,此時由於有任務在執行,就可以觀察這些異步任務的銷燬與Spring容器中其他資源的順序是否安全。

第四步:運行上面的單元測試,我們將碰到下面的異常內容。


 
  1. org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool

  2.    at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:204) ~[spring-data-redis-1.8.10.RELEASE.jar:na]

  3.    at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:348) ~[spring-data-redis-1.8.10.RELEASE.jar:na]

  4.    at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:129) ~[spring-data-redis-1.8.10.RELEASE.jar:na]

  5.    at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:92) ~[spring-data-redis-1.8.10.RELEASE.jar:na]

  6.    at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:79) ~[spring-data-redis-1.8.10.RELEASE.jar:na]

  7.    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:194) ~[spring-data-redis-1.8.10.RELEASE.jar:na]

  8.    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:169) ~[spring-data-redis-1.8.10.RELEASE.jar:na]

  9.    at org.springframework.data.redis.core.RedisTemplate.randomKey(RedisTemplate.java:781) ~[spring-data-redis-1.8.10.RELEASE.jar:na]

  10.    at com.didispace.async.Task.doTaskOne(Task.java:26) ~[classes/:na]

  11.    at com.didispace.async.Task$$FastClassBySpringCGLIB$$ca3ff9d6.invoke(<generated>) ~[classes/:na]

  12.    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.14.RELEASE.jar:4.3.14.RELEASE]

  13.    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:738) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]

  14.    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]

  15.    at org.springframework.aop.interceptor.AsyncExecutionInterceptor$1.call(AsyncExecutionInterceptor.java:115) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]

  16.    at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_151]

  17.    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_151]

  18.    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_151]

  19.    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_151]

  20.    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_151]

  21.    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_151]

  22. Caused by: redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool

  23.    at redis.clients.util.Pool.getResource(Pool.java:53) ~[jedis-2.9.0.jar:na]

  24.    at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226) ~[jedis-2.9.0.jar:na]

  25.    at redis.clients.jedis.JedisPool.getResource(JedisPool.java:16) ~[jedis-2.9.0.jar:na]

  26.    at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:194) ~[spring-data-redis-1.8.10.RELEASE.jar:na]

  27.    ... 19 common frames omitted

  28. Caused by: java.lang.InterruptedException: null

  29.    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014) ~[na:1.8.0_151]

  30.    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2088) ~[na:1.8.0_151]

  31.    at org.apache.commons.pool2.impl.LinkedBlockingDeque.pollFirst(LinkedBlockingDeque.java:635) ~[commons-pool2-2.4.3.jar:2.4.3]

  32.    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:442) ~[commons-pool2-2.4.3.jar:2.4.3]

  33.    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:361) ~[commons-pool2-2.4.3.jar:2.4.3]

  34.    at redis.clients.util.Pool.getResource(Pool.java:49) ~[jedis-2.9.0.jar:na]

  35.    ... 22 common frames omitted

如何解決

原因分析

從異常信息 JedisConnectionException:Couldnotgeta resourcefromthe pool來看,我們很容易的可以想到,在應用關閉的時候異步任務還在執行,由於Redis連接池先銷燬了,導致異步任務中要訪問Redis的操作就報了上面的錯。所以,我們得出結論,上面的實現方式在應用關閉的時候是不優雅的,那麼我們要怎麼做呢?

解決方法

要解決上面的問題很簡單,Spring的 ThreadPoolTaskScheduler爲我們提供了相關的配置,只需要加入如下設置即可:


 
  1. @Bean("taskExecutor")

  2. public Executor taskExecutor() {

  3.    ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();

  4.    executor.setPoolSize(20);

  5.    executor.setThreadNamePrefix("taskExecutor-");

  6.    executor.setWaitForTasksToCompleteOnShutdown(true);

  7.    executor.setAwaitTerminationSeconds(60);

  8.    return executor;

  9. }

說明: setWaitForTasksToCompleteOnShutdown(true)該方法就是這裏的關鍵,用來設置線程池關閉的時候等待所有任務都完成再繼續銷燬其他的Bean,這樣這些異步任務的銷燬就會先於Redis線程池的銷燬。同時,這裏還設置了 setAwaitTerminationSeconds(60),該方法用來設置線程池中任務的等待時間,如果超過這個時候還沒有銷燬就強制銷燬,以確保應用最後能夠被關閉,而不是阻塞住。

完整示例:

讀者可以根據喜好選擇下面的兩個倉庫中查看 Chapter4-1-4項目:

  • Github:https://github.com/dyc87112/SpringBoot-Learning/

  • Gitee:https://gitee.com/didispace/SpringBoot-Learning/

 

推薦閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章