Spring Boot 異步框架@Async @EnableAsync

一、現實場景

在現實的互聯網項目開發中,針對高併發的請求,一般的做法是高併發接口單獨線程池隔離處理。
假設現在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";
    }
}

七,測試結果

線程得到了複用線程得到了複用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章