前面針對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接口)的自動配置和代理機制料饥,具體原因如下:
- Spring的自動裝配(Auto Configuration): Spring Boot利用自動配置(auto-configuration)機制來簡化配置。當它檢測到@EnableAsync注解時朱监,會自動尋找并配置一個TaskExecutor(線程池)來執(zhí)行@Async標記的方法岸啡。如果在應用上下文中存在多個TaskExecutor的Bean,Spring通常會選擇一個合適的Bean作為默認的異步執(zhí)行器赫编。自定義的ThreadPoolTaskExecutor Bean由于是明確配置的巡蘸,因此優(yōu)先級較高,自然成為首選擂送。
- AsyncConfigurer接口: 當我使用@EnableAsync時悦荒,實際上是在告訴Spring去查找實現(xiàn)了AsyncConfigurer接口的配置類。如果我沒有直接實現(xiàn)這個接口并提供自定義配置嘹吨,Spring會使用默認的配置搬味。但是,如果我提供了自定義的ThreadPoolTaskExecutor Bean蟀拷,Spring會認為這是我希望用于異步任務的線程池碰纬。
- Spring AOP代理: @Async注解的方法在運行時會被Spring的AOP(面向切面編程)機制代理。這個代理邏輯會檢查是否有配置好的TaskExecutor问芬,如果有(比如我自定義的ThreadPoolTaskExecutor)悦析,就會使用這個線程池來執(zhí)行方法,從而實現(xiàn)了異步調用此衅。
- 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ū)給出您的指導意見,感謝光羞!