徹底搞懂Web異步編程模型
長期以來,Spring Web MVC 運(yùn)行在 Tomcat、JBoss 等 Servlet 容器上,是我們開發(fā) Web 服務(wù)的主流框架。但你要注意的是,Servlet 容器是阻塞式的,所以 WebMVC 也建立在阻塞 I/O 之上。
換句話說,任何一個請求的響應(yīng)過程都是同步的,需要在服務(wù)器工作線程接收請求、阻塞等待 I/O 以及完成請求處理之后才能返回。
圖 1 同步請求處理過程示意圖
這樣的同步請求處理機(jī)制對普通應(yīng)用場景來說是合適的,但在一些特定場景下,這種同步機(jī)制會存在局限性,需要開發(fā)人員采用異步的方式來處理 Web 請求。這就引出了今天我們要討論的主題,Web 異步編程模型。
讓我們先從 Web 異步處理需求和場景開始說起。
Web 異步處理需求和場景
Web 異步處理的第一個應(yīng)用場景是為了 提升系統(tǒng)性能。
我們知道,同步請求處理機(jī)制采用的是一個請求對應(yīng)一個線程的實(shí)現(xiàn)過程。這樣,系統(tǒng)請求數(shù)量越大,我們就需要創(chuàng)建越多的線程,而線程是一種資源,系統(tǒng)的響應(yīng)能力會隨著資源的消耗而逐漸下降。但異步處理機(jī)制不需要在處理請求時全程保持某一個線程,這樣線程資源就能做到復(fù)用。
圖 2 異步請求處理過程示意圖
接下來是異步處理的第二個應(yīng)用場景,對于有些請求而言,我們實(shí)際上并不關(guān)注請求的返回結(jié)果,也就是說這些請求采用的是一種 即發(fā)即棄(Fire and Forget)模式。
這個模式有點(diǎn)類似于消息中間件的處理過程,請求線程發(fā)送請求然后直接返回。如果采用同步模式,那么請求必須等待服務(wù)端返回。因此,相比于異步處理,同步模式會造成浪費(fèi)。
圖 3 即發(fā)即棄處理過程示意圖
最后,異步處理的第三種場景,在日常開發(fā)過程中, 某個請求需要處理大量業(yè)務(wù)數(shù)據(jù),這也是我們會經(jīng)常碰到的情況。比較典型的例子就是導(dǎo)出數(shù)據(jù)報(bào)表。在這種場景下,如果采用同步模式,很可能會導(dǎo)致出現(xiàn)請求超時。
這時候,合理的解決方案是先對請求做出快速響應(yīng),然后再啟動異步線程來執(zhí)行大數(shù)據(jù)處理邏輯。
圖 4 大數(shù)量請求處理過程示意圖
現(xiàn)在來簡單總結(jié)一下,從三個特定場景的異步模式應(yīng)用中,我們可以看出:
對于傳統(tǒng)請求場景,異步模式能夠確保線程復(fù)用;
對于即發(fā)即棄場景,異步模式能夠節(jié)省系統(tǒng)資源;
而對于大數(shù)量請求場景,異步模式則能夠提高用戶體驗(yàn)。
所以,如果能夠在復(fù)雜的業(yè)務(wù)場景中集成這三種場景中的異步調(diào)用機(jī)制,我們就可以高效處理 Web 請求。
那么,應(yīng)該如何使用異步模式來高效應(yīng)對這些場景呢?Spring 為我們提供了完整的解決方案,我們一起來看一下。
Spring Web 異步編程模型
異步處理的主要優(yōu)勢是調(diào)用方不必等待被調(diào)用方完成執(zhí)行過程,這就需要啟動新的線程。為了在一個新的線程中執(zhí)行目標(biāo)方法,Spring 異步編程模型提供了一個全新的@Async 注解。該注解可以與 JDK 中的 Future 機(jī)制以及線程池進(jìn)行無縫整合。我們先來看這個@Async 注解。
@Async 注解
想要在 Spring 應(yīng)用程序中啟用異步編程模式,我們可以通過@EnableAsync 注解實(shí)現(xiàn)這一目標(biāo)。常見的做法是在 Spring 配置類上添加這一注解。
@Configuration
@EnableAsync
public class SpringConfig { ... }
@Async 注解支持兩種處理模式,即 即發(fā)即棄模式和普通的請求響應(yīng)模式。我們先來看即發(fā)即棄模式的代碼示例。
@Async
public void recordUserHealthData() {
logger.info("Record user health data successfully.");
}
可以看到,我們在一個返回值為 void 的方法上添加了@Async 注解,這樣該方法中將以異步的方式進(jìn)行執(zhí)行。
然后,我們來看一下請求響應(yīng)式的異步方式代碼示例。
@Service
public class HealthService {
@Async
public Future<String> getHealthDescription() throws InterruptedException {
LOGGER.info("Thread id: " + Thread.currentThread().getId());
//睡眠 2 秒
Thread.sleep(2000);
String healthDescription = “health description”;
LOGGER.info(processInfo);
return new AsyncResult<String>(healthDescription);
}
}
可以看到,這里我們在方法入口打印了當(dāng)前的線程 ID,然后讓主線程睡眠 2 秒用來模擬長時間的業(yè)務(wù)處理流程。接著,我們返回異步調(diào)用的結(jié)果對象 AsyncResult。
AsyncResult 是 Spring 框架對 JDK 中 Future 接口的一種實(shí)現(xiàn),我們可以通過 AsyncResult 對象跟蹤異步調(diào)用的結(jié)果。為了更好理解上述方法的執(zhí)行過程,我們有必要先來看看 JDK 中的 Future 對象。
傳統(tǒng)模式調(diào)用和 Future 模式調(diào)用的對比可以參考圖 5。我們看到在 Future 模式調(diào)用過程中,客戶端在向服務(wù)器端發(fā)起請求之后馬上返回,可以繼續(xù)執(zhí)行其他任務(wù)直到服務(wù)器端通知 Future 調(diào)用的結(jié)果,體現(xiàn)了 Future 調(diào)用異步化特點(diǎn)。
圖 5 傳統(tǒng)調(diào)用(左)和 Future 機(jī)制(右)對比示意圖
但原生的 Future 也有同步等待問題,因?yàn)橥ㄟ^ Future 對象直接獲取調(diào)用結(jié)果同樣會導(dǎo)致線程等待。為了解決這個問題,Java 8 中引入了 CompletableFuture 對原生的 Future 進(jìn)行了優(yōu)化,可以直接通過 CompletableFuture 將異步執(zhí)行結(jié)果交給另外一個異步線程來處理。這樣在異步任務(wù)完成后,我們在獲取任務(wù)結(jié)果時則不需要等待。
例如,如果想要在異步執(zhí)行任務(wù)完成之后返回值,那么可以使用 CompletableFuture 的 supplyAsync() 方法,示例代碼如下所示。
@RequestMapping(value = "/health_description")
public CompletableFuture<String> syncHealthDescription () {
CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
return healthService.getHealthDescription().get();
} catch (InterruptedException | ExecutionException e) {
LOGGER.error(e);
}
return"No health description found";
}
});
return completableFuture;
}
WebAsyncTask
前面介紹的@Async 注解實(shí)際上是通用的,我們可以用它來完成包含 Web 請求在內(nèi)的任意場景下的異步處理流程。而隨著 Spring Boot 的誕生,也出現(xiàn)了 WebAsyncTask 這一專門針對 Web 場景下的異步執(zhí)行組件。
相較@Async 注解,WebAsyncTask 為開發(fā)人員提供了更靈活的異步任務(wù)處理機(jī)制,并內(nèi)置了異步回調(diào)、超時處理和異常處理。如果想要初始化一個 WebAsyncTask 對象,我們需要設(shè)置一個超時時間,并啟動一個線程對象。
public WebAsyncTask(long timeout, Callable<V> callable)
基于這一使用方式,我們先來看一下 WebAsyncTask 的簡單示例。
@RequestMapping(value = "task_normal", method = RequestMethod.GET)
public WebAsyncTask<String> task1() {
System.out.println("The main Thread name is " +
Thread.currentThread().getName());
// 此處模擬開啟一個異步任務(wù)
WebAsyncTask<String> task1 = new WebAsyncTask<String>(4 * 1000L, () -> {
System.out.println("The first Thread name is " +
Thread.currentThread().getName());
Thread.sleep(2 * 1000L);
return"task1 executed!";
});
// 任務(wù)執(zhí)行完成時調(diào)用該方法
task1.onCompletion(() -> {
System.out.println("task1 finished!");
});
// 可以繼續(xù)執(zhí)行其他操作
System.out.println("task1 can do other things!");
return task1;
}
可以看到,這里初始化了一個 WebAsyncTask 對象,并設(shè)置任務(wù)的超時時間為 4s。異步任務(wù)執(zhí)行采用 Thread.sleep 方法來進(jìn)行模擬,這里設(shè)置異步線程的睡眠時間為 2s。然后,我們還通過 WebAsyncTask 的 onCompletion() 方法指定了任務(wù)執(zhí)行完成時的回調(diào)函數(shù)。
執(zhí)行以上代碼,我們在控制臺可以得到如下日志信息。
The main Thread name is http-nio-7000-exec-5
task1 can do other things!
The first Thread name is MvcAsync2
task1 finished!
顯然,我們先打印出了主線程的名稱,然后主線程可以繼續(xù)執(zhí)行并返回結(jié)果。然后我們啟動異步線程,并打印出該線程的名稱。當(dāng)異步線程執(zhí)行完畢時,同樣打印出了這一信息。如果你在瀏覽器中訪問這個 HTTP 端點(diǎn),那么可以獲取異步方法的正常返回值"task1 executed!"。
我們接著來看一下如何設(shè)置異常處理回調(diào)的方法,示例代碼如下所示。
@RequestMapping(value = "task_error", method = RequestMethod.GET)
public WebAsyncTask<String> getUserWithError() {
System.out.println("The main Thread name is "
+ Thread.currentThread().getName());
// 此處模擬開啟一個異步任務(wù)
WebAsyncTask<String> task3 = new WebAsyncTask<String>(4 * 1000L, () -> {
System.out.println("The second Thread name is "
+ Thread.currentThread().getName());
int num = 1 / 0;
System.err.println(num);
return"";
});
// 發(fā)生異常時調(diào)用該方法
task3.onError(() -> {
System.err.println(Thread.currentThread().getName());
System.err.println("task3 error occured!");
return"";
});
// 任務(wù)執(zhí)行完成時調(diào)用該方法
task3.onCompletion(() -> {
System.out.println("task3 finished!");
});
// 可以繼續(xù)執(zhí)行其他操作
System.out.println("task3 can do other things!");
return task3;
}
這里設(shè)置了一個 onError() 回調(diào),并通過除 0 操作觸發(fā)了這一回調(diào),結(jié)果如下所示。
The main Thread name is http-nio-7000-exec-10
task3 can do other things!
The second Thread name is MvcAsync4
http-nio-7000-exec-1
task3 error occured!
task3 finished!
這樣,基于 WebAsyncTask 的異步編程模型就介紹完畢了。從上文中我們可以看出,WebAsyncTask 除了能夠?qū)崿F(xiàn)異步調(diào)用,它所提供的異步編程模型充分考慮了異步執(zhí)行過程中可能出現(xiàn)的異常情況和超時機(jī)制。同時,基于回調(diào)的異步處理結(jié)果的獲取過程也顯得非常自然。相比@Async 注解,WebAsyncTask 的功能更加強(qiáng)大。
所以,在日常開發(fā)過程中,我建議你使用這個工具類來實(shí)現(xiàn)對 Web 請求的異步處理。
總結(jié)
今天我們系統(tǒng)分析了在 Web 應(yīng)用程序開發(fā)過程中,如何使用 Spring 框架提供的異步編程能力來提高系統(tǒng)的響應(yīng)性。
我們從異步處理場景講起,引出 Spring 中所提供了@Async 注解,該注解是對異步處理過程的抽象。在具體使用過程中,我們一般結(jié)合 CompletableFuture 來處理異步線程之間的交互過程。同時,針對 Web 開發(fā)場景,Spring 還專門提供了一個 WebAsyncTask 工具類來簡化開發(fā)過程。
在日常開發(fā)過程中,@Async 注解為開發(fā)人員提供的是一種通用型的異步編程,我們可以使用它在應(yīng)用程序的各層組件中添加異步處理機(jī)制。而 WebAsyncTask 則專門面向 Web 請求處理,因此,如果你正在開發(fā) Web 應(yīng)用程序,那么 WebAsyncTask 無疑是你的首選。