Servlet3 -- Servlet异步处理
Servlet 異步處理
異步處理可以極大地提高web 服務器的負載,Servlet 3.0及其后的版本中,添加了異步servlet。
原文:http://www.infoq.com/cn/news/2013/11/use-asynchronous-servlet-improve
眾所周知,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實現成為主流后,解決方案就變得更加簡單、標準化且優雅了。
在開始介紹解決方案前,我們應該更深入地理解問題的細節。還有什么比看源代碼更直接的呢,下面就來看看下面這段代碼:
上面這個Servlet主要完成以下事情:
為了簡化,代碼中將等待部分替換為一個Thread.sleep()調用。
現在,你可能會覺得這就是一個挺不錯的Servlet。在很多情況下,你的理解都是正確的,上述代碼并沒有什么問題,不過當應用的負載變大后就不是這么回事了。
為了模擬負載,我通過JMeter創建了一個簡單的測試,我會啟動2,000個線程,每個線程運行10次,每次都會向/BlockedServlet這個地址發出請求。將這個Servlet部署在Tomcat 7.0.42中然后運行測試,得到如下結果:
- 平均響應時間:19,324ms
- 最快響應時間:2,000ms
- 最慢響應時間:21,869ms
- 吞吐量:97個請求/秒
默認的Tomcat配置有200個工作線程,此外再加上模擬的工作由2,000ms的睡眠時間來表示,這就能比較好地解釋最快與最慢的響應時間了,每個線程都會睡眠2秒鐘。再加上上下文切換的代價,因此97個請求/秒的吞吐量基本上是符合我們的預期的。
對于絕大多數的應用來說,這個吞吐量還算是可以接受的。重點來看看最慢的響應時間與平均響應時間,問題就變得有些嚴重了。經過20秒而不是期待的2秒才能得到響應顯然會讓用戶感到非常不爽。
下面我們來看看另外一種實現,利用Servlet API 3.0的異步支持:
@WebServlet(asyncSupported = true, value = "/AsyncServlet") public class AsyncServlet extends HttpServlet {private static final long serialVersionUID = 1L;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);}@Overridepublic void contextInitialized(ServletContextEvent servletContextEvent) {thread = new Thread(new Runnable() {@Overridepublic void run() {while (true) {try {Thread.sleep(2000);AsyncContext context;while ((context = queue.poll()) != null) {try {ServletResponse response = context.getResponse();response.setContentType("text/plain");PrintWriter out = response.getWriter();out.printf("Thread %s completed the task", Thread.currentThread().getName());out.flush();} catch (Exception e) {throw new RuntimeException(e.getMessage(), e);} finally {context.complete();}}} catch (InterruptedException e) {return;}}}});thread.start();}@Overridepublic 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呢?
----------------
一個實例:
原文: http://www.jsjtt.com/java/JavaWebkaifa/52.html
Servlet3.0在MVC中作為控制器,控制器負責分發任務給MODEL完成,然后把結果交給JSP顯示;而如果有許多MODEL,其中有一個MODEL處理時間很長,則會導致整個頁面的顯示很慢;
異步處理關鍵點:將復雜業務處理另外開一個線程,而Servlet將執行好的業務先送往jsp輸出,等到耗時業務做完后再送往JSP頁面;
就是類似ajax的異步請求處理,避免頁面加載時間過長。
servlet3.0異步處理注意點:需要在Annotation中注明 asyncSupported=true;
下面給出代碼實例:
java代碼:
import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.annotation.*; import java.io.*;@WebServlet(name = "AsyncServlet", urlPatterns = {"/AsyncServlet"}, asyncSupported = true) public class AsyncServlet extends HttpServlet {/** */private static final long serialVersionUID = 2432074905382867541L;public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {request.setCharacterEncoding("GBK");response.setContentType("text/html;charset=GBK");PrintWriter out = response.getWriter();out.println("<html>");out.println("<body>");out.println("====頁面加載開始====<hr />");AsyncContext actx = request.startAsync();actx.setTimeout(30 * 3000);actx.start(new MyThread(actx));out.println("====頁面加載結束====<hr />");out.println("</body>");out.println("</html>");out.flush();} }class MyThread implements Runnable {private AsyncContext actx;public MyThread(AsyncContext actx) {this.actx = actx;}public void run() {try {Thread.sleep(5 * 1000); // 消耗5秒actx.dispatch("/index.jsp");}catch (Exception e) {}} }index.jsp頁面:
<%@ page contentType="text/html;charset=GBK" pageEncoding="GBK" session="false"%> <html> <body> <% out.println("======體驗servlet3.0異步處理效果 ===="); out.println("======用戶處理耗時業務 ===="); %> </body> </html>以上代碼就是servlet3.0的異步處理代碼,要使用tomcat7.0及以上版本才能支持servlet3.0,
要不然javax.servlet.annotation.*會報錯,提示“The import javax.servlet.annotation cannot be resolved”
總結
以上是生活随笔為你收集整理的Servlet3 -- Servlet异步处理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2022 年度中国电竞产业年会将于 2
- 下一篇: ASP.NET MVC Caching