通過Message-Driven beans來添加併發處理

概要:
  在使用J2EE框架的應用程序中添加併發處理往往受到一些嚴格的限制,主要原因有兩個:首先EJB的規範限制了在EJB容器中產生新的用戶線程, 另外, SessionBean的方法是必須被同步調用的. 但是, 爲了保證響應時間,相當多的應用程序必須實現併發處理. 有幾種方法可以克服這種上述的限制,其中消息驅動由於其於生俱來的異步處理能力, 以及通過JMS和Message-Driven beans可以與J2EE應用服務器緊密的結合,成爲了其中最突出的解決方法. 本文章詳細描述瞭如何使用MDBS來爲J2EE應用程序實現併發處理. 


    併發程序能夠同時處理多個任務. 併發改善了程序的數據讀寫吞吐量, 執行速度以及響應速度. 在單處理器系統中, 併發程序通過利用重疊IO讀寫時間來有效的利用了計算機的資源. 在多處理器系統中, 併發程序通過在多個CPU上併發執行程序來最大限度的提高吞吐量.

    有若干方法可以實現併發. 在Java中, 可以通過多線程來實現. 相對於獨立的進程, 線程具有較低的系統開銷. Java對線程提供內在的語言級別的支持, 所以, 併發程序的支持是Java不可缺少和引人注意的一個特點.

   在現實中,我們會發現併發能力對很多應用都是都不可缺少的. 本文要描述的例子是如何在多個零售處搜索某個商品的最低價格及供貨信息. 本文描述了在J2EE架構下的幾種不同的實現,以及在單用戶請求下,如何使用MDBS來實現並行處理. 我們稱該應用爲Price Buster.
    
    爲了證明爲什麼Price Buster需要併發處理, 我們先來討論這個應用是做什麼的,需要哪些功能, 以及在J2EE架構下最好的實現方法. 我們的應用通過Web層接受用戶輸入的貨物名稱或型號來作爲搜索條件, 然後調用後臺的程序來搜索多個供貨數據源該貨物的價格和供應情況, 格式化結果,最後返回給查詢者.
本應用的理想實現是在儘可能短時間內, 搜索儘量多的供貨數據源. 連接多個供貨數據源進行查詢的後臺進程, 佔消耗時間最大一部分.假設在一個數據源查詢某個貨物的價格需要15秒, 那麼, 如果查詢操作是一個接一個的串行進行, 查詢10個數據源至少 需要150秒.對於客戶來說, 150秒的響應時間顯然是不可接受的.  在對多個數據源進行查詢的前提下,  爲了保證響應時間, 查詢操作必須並行執行不是串行. 

    現在讓我們來討論, 在使用J2EE架構下的幾種不同的實現方法.  最典型和通用的方法是由Web模塊和EJB模塊組成. Web模塊由servlets和jsp實現, 負責處理客戶會話和數據顯示, EJB模塊負責連接供貨源進行查詢.整體架構如圖一所示. EJB查詢模塊一個接着一個的從三個供貨源A,B和C獲得貨物的價格, 最重要的一點是查詢操作是串行執行的, 所以將結果顯示給客戶至少需要45秒.

 
按此在新窗口瀏覽圖片
圖一

    在這個實現裏, 用戶的響應時間依賴於供貨數據源的數量和查詢一個數據源所需要的時間. 儘管這個實現提供了正確的功能, 但是有個嚴重的設計缺點, 響應時間隨着供貨源數量的增加而線性增加, 因此查詢20個供貨源至少需要300秒, 這顯然是不可接受的. 

     爲了正確實現功能並且達到理想的響應時間,  本應用必須重新設計, 使用另外的技術使查詢操作是並行進行而不是串行.  換句話說, 我們需要在上面的實現中實現並行操作來獲得更快的響應時間. 如之前所述, 在JAVA程序中, 這個可以通過多線程來實現, 例如爲多個線程分配單獨的任務. 這種技術在所分派的任務是重於IO讀寫而不是重於CPU計算的時候特別有效, 正如Price Buster程序. 供貨源的EJB組件連接數據庫進行IO讀寫, 至少需要15秒的時間, 在這個時間內, EJB組件只等待結果, 而不作任何操作, 因此, 這個程序是最適合使用多線程實現的. 

下面是幾種支持併發的方法:
1.    修改Web模塊的實現, 在Servlet中使用多線程. 每個工作線程可以直接調用EJB組件, 每個工作線程負責對一個供貨數據源的查詢. 當有請求到來時, Servlets根據當前數據源的數量, 生成若干工作線程, 每個線程調用EJB組件進行查詢. 這時Servlet主線程可以等待查詢結果或者進行超時處理.這種方法固然解決了問題, 但是它違背了J2EE基本的設計原則, 就是應用開發人員應該專注於應用邏輯的處理, 而不應該關心多線程和同步處理等系統級的問題.

2.    Search EJB組件可以生成多個線程來並行的調用Retailer EJB組件.不幸的是,這種方法是完全不可取的. 因爲EJB的規範限制在容器裏創建新的用戶線程. 注意EJB容器本身就是實現併發處理, 支持多客戶連接的.這個限制的主要原因是, J2EE技術本身就是提供一個實現強伸縮性的服務器端組件的架構, 是由它來提供併發處理及其他服務的. 這個架構減輕了開發人員實現複雜多線程程序的難度, 容器通過創建線程更能有效的管理資源的分配.

3.    利用MDBs來實現Retailer EJB組件. Search EJB組件可以同時發若干個任務消息給若干Retailer組件, 每個接收到消息的Retailer組件處理任務, 從而實現併發處理. Session Bean不能這麼實現是因爲它們必須是同步並且線性的被調用的. 換句話說, 因爲MDBs可以根據消息事件異步的被調用, 因此可以並行的處理請求. 

    我們要清楚EJB規範的限制. 最佳的實現方法是第三個:異步的調用MDBs. 在不同的系統組件之間進行異步通信, 消息是最通用和可靠的機制. 通過JMS和MDBs技術, 消息處理已經非常緊密的集合在J2EE框架中.本文剩餘的內容將說明如何通過MDBs,在EJB中支持並行處理.
  
    現在我來討論如何通過MDBs爲Price Buster支持併發處理的細節問題. 要使用MDBs, 首先要使用支持EJB2.0規範的應用服務器, 其次要採用支持JMS接口的消息軟件,例如IMB的MQ套件. 最好是選擇能夠和應用服務器很好集成的,例如WebSphere+MQ就是一個不錯的選擇. 

在我們的實現中, 如圖二所示, Retailer 組件是以MDBs形式部署的, 因此是通過消息事件而不是session EJB接口來提供服務的.下面是完整的流程.
 


    首先,客戶通過Jsp/Servlet提供貨物的名稱或是型號, Servlet調用Search EJB組件的方法, Search EJB組件根據輸入構造了三個JMS消息,每個對應一個將要搜索的數據源, 然後將三個消息放入請求隊列. 然後Search EJB組件等待響應隊列上的迴應消息(步驟2). 請求消息觸發Retailer MDBs, 每個Retailer MDB響應一個消息, 同時開始消息的處理.(步驟3). 完成價格和供應信息的搜索, Retailer MDBs將結果包裝在JMS消息中,然後將消息放入響應隊列中(步驟4), 注意所有的Retailer MDBs是並行的處理消息的, 所以它們都是在大致15秒內返回結果. 等待中的Search EJB組件從響應隊列中取出三個迴應消息, 解析結果, 然後返回結果給Servlet/Jsp, 由後者顯示結果.

    在這個方法中, 最短的響應時間取決於單個數據源的響應時間,而不是象第一個方法還要依賴於數據源的數量.因此, 即便數據源增加到10個, 最短的響應時間仍然是將近15秒. 消息的封裝和解釋以及與消息系統的交互只需要花費非常短的時間. 這個響應時間對比串行處理的150秒時間, 是個非常突出的改善.
當有多個客戶同時發起查詢請求的時候, 將有多個Search EJB組件的實例提供服務, 每個服務一個servlet請求. 在這種情況下, 必須有一種機制來映射這組Retailer MDBs與相應的調用他們的EJB組件. 

    有以下幾種方法來實現這種映射:
1.    爲每個請求創建一個臨時的響應隊列. Search EJB 組件可以創建臨時的響應隊列並且將它的名字與請求參數一起包裝在請求消息中, 然後放入請求隊列. Retailer MDBs通過查詢請求消息中的臨時隊列信息, 將響應消息放入響應的臨時響應隊列中. 這個方法簡單的解決了不同請求之間的消息衝突, 但是, 必須要考慮創建臨時隊列的開銷. 如果開銷可以被接受, 那麼這個方法是可取的,它完全消除了多個Search EJB組件之間數據混淆的可能性.

2.   使用響應隊列池. 這個方法類似第一個方法,每個臨時響應隊列是從池中取出, 而不是每次都創建新的. 爲了管理隊列池, 必須開發一個額外的管理類或者是EJB 組件, 由它負責將池中的隊列分派給某個Search EJB 組件實例, 這個管理類還要負責清理池中隊列對象中的無用消息, Search EJB組件在每次處理完畢後,都要將臨時隊列歸還池.

3.    使用JMS 的 Selector機制, 從響應隊列中只取出相應的目標響應消息.JMS允許客戶通過消息頭來指定對自己有意義的消息數據.使用Selector,接收者只會接收到消息頭和其他屬性滿足若干條件的消息.這時只存在一個響應隊列, Search EJB組件創建一個鍵值,然後通過消息頭傳給Retailer MDBs, 然後Search EJB組件利用該鍵值創建一個Selector對象, 所有的Retailer MDBs都會在響應消息頭加入該鍵值.Search EJB組件只會接收到消息頭中包含該鍵值的消息.這個方法的缺點是當響應隊列中的消息很多的時候,速度會下降.

/* Step 1: Create a unique key, use some class, say 
   UniqueKeyCreater. **/

String uniqueKey = UniqueKeyCreater.getKey();
String retailers [] = new String []{"A", "B", "C"};

/* Step 2: Put three messages in the REQUESTQ, one for each retailer.
   Define a header called "KEY" and set its value to uniqueKey            
   Note: The retailer MDBs also have to set the same header/value 
   in the response messages. **/

QueueSender queueSender = queueSession.createSender(requestQueue);
Message message = queueSession.createTextMessage();
message.setStringProperty("KEY",uniqueKey);

for(int i = 0; i < retailers.length; i++) {
     message.setText("Retailer:" + retailers[i] + ",Item:" + itemName);
     queueSender.send(message);
}

/* Step 3: Now wait for messages in the response queue. Get only those 
   messages that have the header KEY with value set to uniqueKey.
   Use a JMS selector for this purpose. The while loop will break if 
all 
   three responses arrive in RESPONSEQ or 30 seconds are over. Even if 
   all three response messages do not arrive in 30 seconds, the code 
   will be out of while loop, ensuring a guaranteed response time of 30 
   seconds. **/

String selector = "KEY = '" + uniqueKey + "'";

QueueReceiver queueReceiver = 
queueSession.createReceiver(responseQueue, selector);

long startTime = System.currentTimeMillis();
int messagesExpected = retailers.length;
long waitTime = 30000; // 30 seconds
Vector responses = new Vector(retailers.length);

while(messagesExpected > 0 && waitTime > 0 ) {

    Message rcvdMsg = queueReceiver.receive(waitTime);

     //Check if we got a msg, if not then break.
      if (rcvdMsg == null){
        //Wait time expired.
         break;
      }
      responses.add(rcvdMsg);

      messagesExpected--;

      waitTime = 30000 - (System.currentTimeMillis() - startTime);
}

    每個討論過的方法都有各自的長處和缺點, 不同的應用, 都有各自適合的解決方法. 而合適的解決方案依賴於許多因素,例如負載峯值, 創建臨時隊列的開銷, JMS服務的性能,服務器的配置等等.  有時候, 也可以將第二和第三種方法的混合使用,來用於一些大型的應用.

    到目前爲止, 我們討論的都是並行處理是如何改善基於Web的應用的響應時間,但是使用並行處理也有另外的副作用: 應用程序的運行中,短時間內資源的開銷提高很多. 對比使用串行處理機制的程序, 並行處理的負載峯值高很多. 
在Price Buster應用中, 如果有20個客戶同時發起請求, 存在10個數據源,那麼,在非MDB的實現中, 將有20個Retailer EJB組件 , 而在MDB實現中,存在200個Retailer MDB. 因此, 後者的應用服務器需要提供更強力的性能, 不過,單是一個能夠支持上千併發線程的服務器也不能完全解決問題, 因爲併發處理同時提高了對數據源存儲系統的負載峯值的要求.

    基於MDB實現併發的系統除了能夠提高響應時間, 還有個更重要的作用就是能夠保證響應時間.舉個例子, 對於不支持併發或者是非基於MDB實現的系統, 當系統運行正常的時候, 如果有三個查詢數據源,那麼響應時間大概在45秒, 但如果其中一個數據源出現故障, 響應時間延遲到200秒怎麼辦? 這個時間顯然是不能接受的. 這時候一般會忽略出錯的那個數據源,只返回正常的兩個數據源的查詢結果. 但不幸的是, 對於非MDB實現的系統, 並不容易做到這一點. 因爲Search EJB組件對於Retailer EJB組件的調用是同步堵塞的, 所以是沒有辦法中斷的. 當然, 如果只存在一個數據源, 那麼固定響應時間的問題會容易解決很多. 但實際應用很多時候並非如此. 

    然而, 如果是基於MDB實現的系統,上面的問題就很容易解決了. Search EJB組件在發送完請求消息後, 就等待迴應消息, 在一定時間內如果沒有得到響應就超時返回. 可以通過調用queueReceiver對象的receive(long waitimeout)方法, 來設置等待的時間. 如果沒有設置超時時間, 那麼只有當收到響應消息的時候, 該方法纔會返回. 爲了保證固定的30秒響應時間, 可以把最長等待時間設置爲28-29秒. 

    同時也要爲組織和格式化結果預留一些時間. 即便不是所有的數據源都能在指定時間內返回查詢結果, 程序也能夠在30秒內返回數據. 對於那些速度太慢的數據源所返回的結果可以忽略不計. 對於一個存在多個數據源的系統來說, 應該要設置一個最長的等待時間, 因爲不能永遠保證所有的數據源都運行正常. 調用外部系統服務的時候, 總是應該設置一個超時時間的. 

    從上面的討論可以看到, 對於那些大型的IO操作頻繁的應用, 如搜索引擎, 併發的支持是不可缺少. 使用JMS和MDBs, 可以很容易的爲基於J2EE架構的這類應用增加併發的支持. 基於MDB的解決方案有兩個最主要的好處:
1. 對比線性處理, 支持併發處理, 提供更快的響應時間.
2. 保證固定的響應時間.


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