衆所周知,servlet 3.0标準已經釋出了很長一段時間,相較于之前的2.5版的标準,新标準增加了很多特性,比如說以注解形式配置servlet、web.xml片段、異步處理支援、檔案上傳支援等。雖然說現在的很多java web項目并不會直接使用servlet進行開發,而是通過如spring mvc、struts2等架構來實作,不過這些java web架構本質上還是基于傳統的jsp與servlet進行設計的,是以servlet依然是最基礎、最重要的标準群組件。在servlet 3.0标準新增的諸多特性中,異步處理支援是令開發者最為關注的一個特性,本文就将詳細對比傳統的servlet與異步servlet在開發上、使用上、以及最終實作上的差别,分析異步servlet為何會提升java web應用的性能。
本文主要介紹的是能夠解決現代web應用常見性能問題的一種性能優化技術。當今的應用已經不僅僅是被動地等待浏覽器來發起請求,而是由應用自身發起通信。典型的示例有聊天應用、拍賣系統等等,實際情況是大多數時間與浏覽器的連接配接都是空閑的,等待着某個事件來觸發。
這種類型的應用自身存在着一個問題,特别是在高負載的情況下問題會變得更為嚴重。典型的症狀有線程饑餓、影響使用者互動等等。根據近一段時間的經驗,我認為可以通過一種相對比較簡單的方案來解決這個問題。在servlet api 3.0實作成為主流後,解決方案就變得更加簡單、标準化且優雅了。
在開始介紹解決方案前,我們應該更深入地了解問題的細節。還有什麼比看源代碼更直接的呢,下面就來看看下面這段代碼:
@webservlet(urlpatterns = "/blockingservlet")
public class blockingservlet extends httpservlet {
private static final long serialversionuid = 1l;
protected void doget(httpservletrequest request,
httpservletresponse response) throws servletexception, ioexception {
try {
long start = system.currenttimemillis();
thread.sleep(2000);
string name = thread.currentthread().getname();
long duration = system.currenttimemillis() - start;
response.getwriter().printf("thread %s completed the task in %d ms.", name, duration);
} catch (exception e) {
throw new runtimeexception(e.getmessage(), e);
}
上面這個servlet主要完成以下事情:
請求到達,表示開始監控某些事件。
線程被阻塞,直到事件發生為止。
在接收到事件後,編輯響應然後将其發回給用戶端。
為了簡化,代碼中将等待部分替換為一個thread.sleep()調用。
現在,你可能會覺得這就是一個挺不錯的servlet。在很多情況下,你的了解都是正确的,上述代碼并沒有什麼問題,不過當應用的負載變大後就不是這麼回事了。
平均響應時間:19,324ms
最快響應時間:2,000ms
最慢響應時間:21,869ms
吞吐量:97個請求/秒
對于絕大多數的應用來說,這個吞吐量還算是可以接受的。重點來看看最慢的響應時間與平均響應時間,問題就變得有些嚴重了。經過20秒而不是期待的2秒才能得到響應顯然會讓使用者感到非常不爽。 下面我們來看看另外一種實作,利用servlet api 3.0的異步支援:
@webservlet(asyncsupported = true, value = "/asyncservlet")
public class asyncservlet extends httpservlet {
protected void doget(httpservletrequest request, httpservletresponse response) throws servletexception, ioexception {
work.add(request.startasync());
public class work implements servletcontextlistener {
private static final blockingqueue queue = new linkedblockingqueue();
private volatile thread thread;
public static void add(asynccontext c) {
queue.add(c);
@override
public void contextinitialized(servletcontextevent servletcontextevent) {
thread = new thread(new runnable() {
public void run() {
while (true) {
asynccontext context;
while ((context = queue.poll()) != null) {
servletresponse response = context.getresponse();
response.setcontenttype("text/plain");
printwriter out = response.getwriter();
out.printf("thread %s completed the task", thread.currentthread().getname());
out.flush();
} finally {
context.complete();
} catch (interruptedexception e) {
return;
});
thread.start();
public void contextdestroyed(servletcontextevent servletcontextevent) {
thread.interrupt();
上面的代碼看起來有點複雜,是以在開始分析這個解決方案的細節資訊之前,我先來概述一下這個方案:速度上提升了75倍,吞吐量提升了20倍。看到這個結果,你肯定迫不及待地想知道這個示例是如何做到的吧。
這個servlet本身是非常簡單的。需要注意兩點,首先是聲明servlet支援異步方法調用:
@webservlet(asyncsupported = true, value = "/asyncservlet")
其次,重要的部分實際上是隐藏在下面這行代碼調用中的。
work.add(request.startasync());
整個請求處理都被委托給了work類。請求上下文是通過asynccontext執行個體來儲存的,它持有容器提供的請求與響應對象。
現在來看看第2個,也是更加複雜的類,work類實作了servletcontextlistener接口。進來的請求會在該實作中排隊等待通知,通知可能是上面提到的拍賣中的競标價,或是所有請求都在等待的群組聊天中的下一條消息。
當通知到達時,我們這裡依然是通過thread.sleep()讓線程睡眠2,000ms,隊列中所有被阻塞的任務都是由一個工作線程來處理的,該線程負責編輯與發送響應。相對于阻塞成百上千個線程以等待外部通知,我們通過一種更加簡單且幹淨的方式達成所願,通過批處理在單獨的線程中處理請求。
還是讓結果來說話吧,測試配置與方才的示例一樣,依然使用tomcat 7.0.24的預設配置,測試結果如下所示:
平均響應時間:265ms
最快響應時間:6ms
最慢響應時間:2,058ms
吞吐量:1,965個請求/秒
雖然說這個示例很簡單,不過對于實際項目來說通過這種方式依然能獲得類似的結果。
在将所有的servlet改寫為異步servlet前,請容許我多說幾句。該解決方案非常适合于某些應用場景,比如說群組通知與拍賣價格通知等。不過,對于等待資料庫查詢完成的請求來說,這種方式就沒有什麼必要了。像往常一樣,我必須得重申一下——請通過實驗進行度量,而不是瞎猜。
對于那些不适合于這種解決方案的場景來說,我還是要說一下這種方式的好處。除了在吞吐量與延遲方面帶來的顯而易見的改進外,這種方式還可以在大負載的情況下優雅地避免可能出現的線程饑餓問題。
另一個重要的方面,這種異步處理請求的方式已經是标準化的了。它不依賴于你所使用的servlet api 3.0,相容于各種應用伺服器,如tomcat 7、jboss 6或是jetty 8等,在這些伺服器上這種方式都可以正常使用。你不必再面對各種不同的comet實作或是依賴于平台的解決方案了,比如說weblogic futureresponseservlet。
就如本文一開始所提的那樣,現在的java web項目很少會直接使用servlet api進行開發了,不過諸多的web mvc架構都是基于servlet與jsp标準實作的,那麼在你的日常開發中,是否使用過出現多年的servlet api 3.0,使用了它的哪些特性與api呢?
最新内容請見作者的github頁:http://qaseven.github.io/