SpringBoot使用非同步執行緒池實現生產環境批量資料推送

福隆苑居士發表於2022-01-30

 

前言

SpringBoot使用非同步執行緒池:

1、編寫執行緒池配置類,自定義一個執行緒池;

2、定義一個非同步服務;

3、使用@Async註解指向定義的執行緒池;

 

這裡以我工作中使用過的一個案例來做描述,我所在公司是醫療行業,敏感資料需要上報到某監管平臺,所以有一個定時任務在流量較小時(一般是凌晨後)執行上報行為。但特殊時期會存在一定要在工作時間大批量上報資料的情況,且要求短時間內就要完成,此時就考慮寫一個專門的非同步上報介面手動執行,利用執行緒池上報,極大提高了速度。

 


 

  • 編寫執行緒池配置類

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    
    import java.util.concurrent.Executor;
    import java.util.concurrent.ThreadPoolExecutor;
    
    /**
     * 類名稱:ExecutorConfig
     * ********************************
     * <p>
     * 類描述:執行緒池配置
     *
     * @author guoj
     * @date 2021-09-07 09:00
     */
    @Configuration
    @EnableAsync
    @Slf4j
    public class ExecutorConfig {
        /**
         * 定義資料上報執行緒池
         * @return
         */
        @Bean("dataCollectionExecutor")
        public Executor dataCollectionExecutor() {
    
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    
            // 核心執行緒數量:當前機器的核心數
            executor.setCorePoolSize(
                    Runtime.getRuntime().availableProcessors());
    
            // 最大執行緒數
            executor.setMaxPoolSize(
                    Runtime.getRuntime().availableProcessors() * 2);
    
            // 佇列大小
            executor.setQueueCapacity(Integer.MAX_VALUE);
    
            // 執行緒池中的執行緒名字首
            executor.setThreadNamePrefix("sjsb-");
    
            // 拒絕策略:直接拒絕
            executor.setRejectedExecutionHandler(
                    new ThreadPoolExecutor.AbortPolicy());
    
            // 執行初始化
            executor.initialize();
    
            return executor;
        }
    
    }

     PS:

    1)、需要注意,這裡一定要自己定義ThreadPoolTaskExecutor執行緒池,否則springboot的非同步註解會執行預設執行緒池,存線上程阻塞導致CPU飆高及記憶體溢位的風險。這一點可以參考阿里開發手冊,執行緒池定義這塊明確提到了這一點;

    2)、在@Bean註解中定義執行緒池名稱,後面非同步註解會用到。

     


     

  • 編寫非同步服務

    /**
     * 非同步方法的服務, 不影響主程式執行。
     */
    @Service
    public class AsyncService {
    
        private final Logger log = LoggerFactory.getLogger(AsyncService.class);
    
        /**
         * 傳送簡訊
         */
        @Async("sendMsgExecutor")
        public void sendMsg(String access_token, Consult item, Map<String, String> configMap) {
            // 此處編寫傳送簡訊業務
            // 1、buildConsultData();
            // 2、sendMsg();
        }
    
        /**
         * 傳送微信訂閱訊息
         */
        @Async
        public void sendSubscribeMsg(String access_token, Consult item, Map<String, String> configMap) {
            // 此處編寫傳送微信訂閱訊息業務
            // 1、buildConsultData();
            // 2、sendSubscribeMsg();
        }
    
        /**
         * 資料並上報
         */
        @Async("dataCollectionExecutor")
        public void buildAndPostData(String access_token, Consult item, Map<String, String> configMap) {
            // 此處編寫上報業務,如拼接資料,然後執行上報。
            // 1、buildConsultData();
            // 2、postData();
        }
    }
PS:
1)、以上是程式碼片段,個人經驗認為專門定義一個非同步service存放各個非同步方法最佳,這樣可以避免編碼時一些誤操作比如非同步方法不是void或者是private修飾,導致@Async註解失效的情況,同時可以安排每個註解指向不同的自定義執行緒池更加靈活;
2)、@Async註解中的名稱就是上面定義的自定義執行緒池名稱,這樣業務執行時就會從指定執行緒池中獲取非同步執行緒。

 

  • 非同步批量上報資料

    @Autowired
    private AsyncService asyncService;
    
    /**
     * 手動上報問診記錄,執行緒池方式。
     */
    public void manualUploadConsultRecordsAsync(String channel, Date startTime, Date endTime) {
    
        // 查詢指定時間內的問診記錄
       List<Consult> consultList = consultService
           .findPaidListByChannelAndTime(channel, startTime, endTime, configMap.get("serviceId"));
    
       if (!CollectionUtils.isEmpty(consultList)) {
    
           log.debug("[SendWZDataService][manualUploadConsultRecordsAsync]>>>> 手動上報問診記錄, 一共[{}]條", consultList.size());
    
           consultList.forEach((item) -> {
               try {
                   // 非同步呼叫,使用執行緒池。
                   asyncService.buildAndPostData(access_token, item, configMap);
               } catch (Exception ex) {
                   log.error("[SendWZDataService][manualUploadConsultRecordsAsync]>>>> 手動上報問診記錄發生異常: ", ex);
               }
           });
    
       }
    }

     


     

    •  總結

      以上方式已經在生產環境執行,在工作時間內執行過很多次,一次數萬條記錄基本是幾分鐘內就全部上報完畢,而正常迴圈遍歷時一次大概需要半個小時左右。
      執行緒池的使用方式往往來源於業務場景,如果類似的業務不存在緊急處理的情況,大體還是以任務排程執行為主,因為更安全。如果存在緊急處理的情況,那麼使用SpringBoot+執行緒池的方式不僅能節省非常多的時間,且不佔用主執行緒的執行空間。
      喜歡就點個關注吧~~

相關文章