springboot優(yōu)雅shutdown時異步線程安全優(yōu)化

前面針對graceful shutdown寫了兩篇文章
第一篇:
https://blog.csdn.net/chenshm/article/details/139640775
只考慮了阻塞線程力穗,沒有考慮異步線程
第二篇:
https://blog.csdn.net/chenshm/article/details/139702105
第二篇考慮了多線程的安全性曲尸,包括異步線程荚守。

1. 為什么還需要優(yōu)化呢?

因為第二篇的寫法還不夠優(yōu)美歉秫,它存在以下缺陷据沈。

  • 只在一個service bean 里面對ExecutorService做predestroy,只能對一個service類的異步線程提供安全保障,其他service類的異步業(yè)務需要重寫predestroy的邏輯杯矩,造成代碼冗余。
  • 異步方法的寫法比較麻煩袖外,其他程序員并不常用∈仿。現(xiàn)在用springboot的程序員喜歡用@Async注解,隨時隨地可以把方法變成異步執(zhí)行曼验。
    從架構師的角度考慮的話泌射,寫代碼盡量滿足多數(shù)情況可用,易用鬓照,最好還是全局有效的熔酷,讓其他程序員專注于寫業(yè)務代碼。
    接下來讓我們實現(xiàn)@Async注解的異步方法在app graceful shutdow時保持線程安全豺裆。

2. 代碼優(yōu)化

  • 確認graceful shutdown settings

    graceful shutdown settings for springboot

  • 添加第一個servcie 的異步方法

package com.it.sandwich.service.impl;

import com.it.sandwich.service.Demo2Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

/**
 * @Author 公眾號: IT三明治
 * @Date 2024/6/16
 * @Description:
 */
@Slf4j
@Service
@Component
public class Demo1ServiceImpl implements Demo1Service {
    @Override
    @Async
    public void feedUserInfoToOtherService(String userId) throws InterruptedException {
        for (int i = 0; i < 35; i++) {
            log.info("Demo1Service update {} login info to other services, service num: {}", userId, i+1);
            Thread.sleep(1000);
        }
    }
}
  • 添加第二個Servcie 的異步方法
package com.it.sandwich.service.impl;

import com.it.sandwich.service.Demo2Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

/**
 * @Author 公眾號: IT三明治
 * @Date 2024/6/16
 * @Description:
 */
@Slf4j
@Service
@Component
public class Demo2ServiceImpl implements Demo2Service {
    @Override
    @Async
    public void feedUserInfoToOtherService(String userId) throws InterruptedException {
        for (int i = 0; i < 40; i++) {
            log.info("Demo2Service update {} login info to other services, service num: {}", userId, i+1);
            Thread.sleep(1000);
        }
    }
}

添加兩個@Async方法拒秘,驗證全局生效。

  • api接口
package com.it.sandwich.controller;

import com.it.sandwich.base.ResultVo;
import com.it.sandwich.service.Demo1Service;
import com.it.sandwich.service.Demo2Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @Author 公眾號: IT三明治
 * @Date 2024/6/16
 * @Description:
 */
@Slf4j
@RestController
@RequestMapping("/api")
public class DemoController {

    @Resource
    Demo1Service demo1Service;
    @Resource
    Demo2Service demo2Service;

    @GetMapping("/{userId}")
    public ResultVo<Object> getUserInfo(@PathVariable String userId) throws InterruptedException {
        log.info("userId:{}", userId);
        demo1Service.feedUserInfoToOtherService(userId);
        demo2Service.feedUserInfoToOtherService(userId);
        for (int i = 0; i < 30; i++) {
            log.info("updating user info for {}, waiting times: {}", userId, i+1);
            Thread.sleep(1000);
        }
        return ResultVo.ok();
    }
}
  • @Async有效的全局線程池配置
package com.it.sandwich.config;

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;


/**
 * @Author 公眾號: IT三明治
 * @Date 2024/6/16
 * @Description:
 */
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2); // 設置核心線程數(shù)
        executor.setMaxPoolSize(5); // 設置最大線程數(shù)
        executor.setQueueCapacity(100); // 設置隊列容量
        executor.setThreadNamePrefix("sandwich-async-pool-"); // 自定義線程名稱前綴
        executor.setWaitForTasksToCompleteOnShutdown(true); // 設置線程池關閉時是否等待任務完成
        executor.setAwaitTerminationSeconds(60); // 設置等待時間臭猜,如果你需要所有異步線程的安全退出躺酒,請根據(jù)線程池內敢長線程處理時間配置這個時間
        return executor;
    }
}

3. 驗證代碼

  • 重啟服務
  • call api
Administrator@USER-20230930SH MINGW64 /d/git/micro-service-logs-tracing
$ curl http://localhost:8080/api/sandwich
  • shutdown app(Ctrl+F2)
  • 驗證日志
    查看日志前我們先分析一下代碼,我們一個api請求里面一共有三個線程蔑歌,一個阻塞線程羹应,兩個@Async注解修飾的異步線程。阻塞線程的循環(huán)計數(shù)日志從1到30次屠,Demo1Service 異步線程的循環(huán)計數(shù)日志從1到35园匹,Demo2Service異步線程的循環(huán)計數(shù)日志從1到40雳刺。我們期待的結果是提前shutdown之后三個線程的計數(shù)日志都完整打印出來。
    graceful shutdown logs for three threads

日志完美驗證了我們的期待裸违。 我設置的“sandwich-async-pool-”線程名前綴也在兩個線程日志中體現(xiàn)了煞烫。進一步證明AsyncConfig對所有@Async注解修飾的異步線程全局有效。
這是為什么呢累颂?

4. AsyncConfig配置代碼分析

當我在Spring配置中通過@Bean定義了一個ThreadPoolTaskExecutor實例滞详,并且在同一配置類或其他被掃描到的配置類中啟用了@EnableAsync注解時,這個自定義線程池會自動與Spring的異步任務執(zhí)行機制關聯(lián)起來紊馏。這一過程背后的原理涉及到Spring的異步任務執(zhí)行器(AsyncConfigurer接口)的自動配置和代理機制料饥,具體原因如下:

  1. Spring的自動裝配(Auto Configuration): Spring Boot利用自動配置(auto-configuration)機制來簡化配置。當它檢測到@EnableAsync注解時朱监,會自動尋找并配置一個TaskExecutor(線程池)來執(zhí)行@Async標記的方法岸啡。如果在應用上下文中存在多個TaskExecutor的Bean,Spring通常會選擇一個合適的Bean作為默認的異步執(zhí)行器赫编。自定義的ThreadPoolTaskExecutor Bean由于是明確配置的巡蘸,因此優(yōu)先級較高,自然成為首選擂送。
  2. AsyncConfigurer接口: 當我使用@EnableAsync時悦荒,實際上是在告訴Spring去查找實現(xiàn)了AsyncConfigurer接口的配置類。如果我沒有直接實現(xiàn)這個接口并提供自定義配置嘹吨,Spring會使用默認的配置搬味。但是,如果我提供了自定義的ThreadPoolTaskExecutor Bean蟀拷,Spring會認為這是我希望用于異步任務的線程池碰纬。
  3. Spring AOP代理: @Async注解的方法在運行時會被Spring的AOP(面向切面編程)機制代理。這個代理邏輯會檢查是否有配置好的TaskExecutor问芬,如果有(比如我自定義的ThreadPoolTaskExecutor)悦析,就會使用這個線程池來執(zhí)行方法,從而實現(xiàn)了異步調用此衅。
  4. Bean的命名和類型匹配: 默認情況下强戴,Spring在查找執(zhí)行器時會優(yōu)先考慮那些名為taskExecutor的Bean,這也是為什么在配置ThreadPoolTaskExecutor時通常會使用這個名字炕柔。當然酌泰,即使不叫這個名字,也可以通過實現(xiàn)AsyncConfigurer接口并重寫getAsyncExecutor方法來指定使用的線程池匕累。

綜上所述陵刹,自定義的ThreadPoolTaskExecutor之所以能成為Spring異步任務執(zhí)行的默認線程池,是因為Spring的自動配置邏輯欢嘿、AOP代理機制以及通過配置明確指定了這個線程池的使用衰琐。
至此也糊,graceful shutdown已經(jīng)可以使多線程,高并發(fā)的項目在做release的時候羡宙,線程安全性得到保障狸剃。 特別是一些長處理的schedul job項目(其中好多job為了提交效率,用了異步機制)狗热,經(jīng)過這樣優(yōu)化之后钞馁,release的信心是不是增強了好多。
寫文章不容易匿刮,如果對您有用僧凰,請點個關注支持一下博主再走。謝謝熟丸。
如果有更好見解的朋友训措,請在評論區(qū)給出您的指導意見,感謝光羞!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末绩鸣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子纱兑,更是在濱河造成了極大的恐慌呀闻,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件萍启,死亡現(xiàn)場離奇詭異总珠,居然都是意外死亡,警方通過查閱死者的電腦和手機勘纯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來钓瞭,“玉大人驳遵,你說我怎么就攤上這事∩轿校” “怎么了堤结?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鸭丛。 經(jīng)常有香客問我竞穷,道長,這世上最難降的妖魔是什么鳞溉? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任瘾带,我火速辦了婚禮,結果婚禮上熟菲,老公的妹妹穿的比我還像新娘看政。我一直安慰自己朴恳,他們只是感情好,可當我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布允蚣。 她就那樣靜靜地躺著于颖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嚷兔。 梳的紋絲不亂的頭發(fā)上森渐,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天,我揣著相機與錄音冒晰,去河邊找鬼章母。 笑死,一個胖子當著我的面吹牛翩剪,可吹牛的內容都是我干的乳怎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼前弯,長吁一口氣:“原來是場噩夢啊……” “哼蚪缀!你這毒婦竟也來了?” 一聲冷哼從身側響起恕出,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤询枚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后浙巫,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體金蜀,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年的畴,在試婚紗的時候發(fā)現(xiàn)自己被綠了渊抄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡丧裁,死狀恐怖护桦,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情煎娇,我是刑警寧澤二庵,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站缓呛,受9級特大地震影響催享,放射性物質發(fā)生泄漏。R本人自食惡果不足惜哟绊,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一因妙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦兰迫、人聲如沸信殊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涡拘。三九已至,卻和暖如春据德,著一層夾襖步出監(jiān)牢的瞬間鳄乏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工棘利, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留橱野,地道東北人。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓善玫,卻偏偏與公主長得像水援,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子茅郎,可洞房花燭夜當晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內容