通过Message-Driven beans来添加并发处理

来源:互联网 发布:服装图案设计软件 编辑:程序博客网 时间:2024/05/17 12:01

 转贴自  http://blog.csdn.net/seastar_pickle/articles/307677.aspx

 

概要:
  在使用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. 保证固定的响应时间.