一、現實場景
在現實的互聯網項目開發中,針對高併發的請求,一般的做法是高併發接口單獨線程池隔離處理。
假設現在2個高併發接口:
一個是修改用戶信息接口,刷新用戶redis緩存.
一個是下訂單接口,發送app push信息.
設計解決方案用於[刷新用戶redis緩存]和[發送app push信息]
二、爲什麼要用異步框架,它解決什麼問題?
在SpringBoot的日常開發中,一般都是同步調用的。但經常有特殊業務需要做異步來處理,例如:註冊新用戶,送100個積分,或下單成功,發送push消息等等。
就拿註冊新用戶爲什麼要異步處理?
- 第一個原因:容錯性、健壯性,如果送積分出現異常,不能因爲送積分而導致用戶註冊失敗;
因爲用戶註冊是主要功能,送積分是次要功能,即使送積分異常也要提示用戶註冊成功,然後後面在針對積分異常做補償處理。 - 第二個原因:提升性能,例如註冊用戶花了20毫秒,送積分花費50毫秒,如果用同步的話,總耗時70毫秒,用異步的話,無需等待積分,故耗時20毫秒。
故,異步能解決2個問題,性能和容錯性。
三、SpringBoot異步調用
在SpringBoot中使用異步調用是很簡單的,只需要使用@Async註解即可實現方法的異步調用。注意:只能是外部調用方法纔可以異步執行,在對象裏面的方法調用不會生效
四、@Async異步調用例子
步驟1:開啓異步任務
採用@EnableAsync來開啓異步任務支持,另外需要加入@Configuration來把當前類加入springIOC容器中。
@Configuration
@EnableAsync
public class RedisSyncConfiguration {
@Bean(name = "redisPoolTaskExecutor")
public ThreadPoolTaskExecutor getRedisPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//核心線程數
taskExecutor.setCorePoolSize(3);
//線程池維護線程的最大數量,只有在緩衝隊列滿了之後纔會申請超過核心線程數的線程
taskExecutor.setMaxPoolSize(100);
//緩存隊列
taskExecutor.setQueueCapacity(50);
//許的空閒時間,當超過了核心線程出之外的線程在空閒時間到達之後會被銷燬
taskExecutor.setKeepAliveSeconds(200);
//異步方法內部線程名稱
taskExecutor.setThreadNamePrefix("redis-");
/**
* 當線程池的任務緩存隊列已滿並且線程池中的線程數目達到maximumPoolSize,如果還有任務到來就會採取任務拒絕策略
* 通常有以下四種策略:
* ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
* ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。
* ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
* ThreadPoolExecutor.CallerRunsPolicy:重試添加當前的任務,自動重複調用 execute() 方法,直到成功
*/
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
@Bean(name = "pushAppMsgPoolTaskExecutor")
public ThreadPoolTaskExecutor getPushAppMsgPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//核心線程數
taskExecutor.setCorePoolSize(2);
//線程池維護線程的最大數量,只有在緩衝隊列滿了之後纔會申請超過核心線程數的線程
taskExecutor.setMaxPoolSize(10);
//緩存隊列
taskExecutor.setQueueCapacity(20);
//許的空閒時間,當超過了核心線程出之外的線程在空閒時間到達之後會被銷燬
taskExecutor.setKeepAliveSeconds(50);
//異步方法內部線程名稱
taskExecutor.setThreadNamePrefix("pushAppMsg-");
/**
* 當線程池的任務緩存隊列已滿並且線程池中的線程數目達到maximumPoolSize,如果還有任務到來就會採取任務拒絕策略
* 通常有以下四種策略:
* ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
* ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。
* ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
* ThreadPoolExecutor.CallerRunsPolicy:重試添加當前的任務,自動重複調用 execute() 方法,直到成功
*/
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}
步驟2:在方法上標記異步調用
增加一個service類,用來做積分處理。
@Async添加在方法上,代表該方法爲異步處理。
@Service
@Slf4j
public class ScoreService {
@Async("redisPoolTaskExecutor")
public void refreshUserRedis() {
//TODO 模擬睡5秒,用於刷新用戶redis緩存
try {
Thread.sleep(1000 * 5);
log.info("-----------refresh user redis---------");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Async("pushAppMsgPoolTaskExecutor")
public void pushAppMsg() {
//TODO 模擬睡10秒,發送app push信息
try {
Thread.sleep(1000 * 10);
log.info("-----------push App message--------------------");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
五、爲什麼要給@Async自定義線程池?
@Async註解,在默認情況下用的是SimpleAsyncTaskExecutor線程池,該線程池不是真正意義上的線程池,因爲線程不重用,每次調用都會新建一條線程。
可以通過控制檯日誌輸出查看,每次打印的線程名都是[task-1]、[task-2]、[task-3]、[task-4]…遞增的。
@Async註解異步框架提供多種線程
SimpleAsyncTaskExecutor:不是真的線程池,這個類不重用線程,每次調用都會創建一個新的線程。
SyncTaskExecutor:這個類沒有實現異步調用,只是一個同步操作。只適用於不需要多線程的地方
ConcurrentTaskExecutor:Executor的適配類,不推薦使用。如果ThreadPoolTaskExecutor不滿足要求時,才用考慮使用這個類
ThreadPoolTaskScheduler:可以使用cron表達式
ThreadPoolTaskExecutor :最常使用,推薦。 其實質是對java.util.concurrent.ThreadPoolExecutor的包裝
六、Ctroller Test?
@RestController
@Slf4j
public class TestController {
@Autowired
private ScoreService scoreService;
@RequestMapping("/sync")
public String createUser() {
System.out.println("mian buisness handler.... done");
log.info("Main business function had been done!");
this.scoreService.refreshUserRedis();
this.scoreService.pushAppMsg();
return "OK";
}
}
七,測試結果
線程得到了複用