前言
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(); } }
-
非同步批量上報資料
@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+執行緒池的方式不僅能節省非常多的時間,且不佔用主執行緒的執行空間。 喜歡就點個關注吧~~
-