技术开发 频道

使用BMT消息驱动BEAN和SPRING进行息处理

【IT168 技术文档】

 简介
 
  自从被引入EJB 2.0规范后,消息驱动bean (MDB)已经成为几乎所有企业Java项目的基础。MDB拥有一个简单而整洁的API,用户不需要创建或生成home和local/remote接口,所有这些使得Java开发社区迅速且广泛地接受了MDB。如您所知,几乎任何大型J2EE项目都有一个重要部分是与其它系统的集成有关的。一种集成不同系统的可取方法是使用面向消息中间件(MOM)提供的企业队列(queue)和主题(topic)基础架构,这要求所有的J2EE服务器都拥有自己的兼容JMS的MOM实现。
 
  看起来大部分企业J2EE项目取决于JMS消息的接收和/或发送。因此,J2EE架构设计者和开发者就一定要熟悉不同的消息消费和生产方法,因为这可能最终决定项目的成功或失败。最近,开始出现一些MDB模型的替代方案,例如Spring和ActiveMQ的消息驱动POJO。当前的MDB模型除了本身是一项有趣有用的技术外,其地位也得以确定,它足够简单,并且在J2EE社区中得到了广泛的采用。
 
  本文主要关注基于MDB的传统消费模型。本文将展示如何利用一种更高效的非事务型消息检索来重构这一场景,而同时仍然保持业务用例的once-and-only-once服务质量(QoS);换句话说,业务代码应该只处理消息一次,并且不应该丢失任何消息。这是最严格的服务质量,也是实现中最有趣的部分。
 
传统的消息消费模型
 
  可以说,在大多数的J2EE项目中,只要出现消费JMS消息的用例,都会使用MDB。除了最普通的情况外,如果从业务角度来看丢失传入的消息和/或处理消息副本可以接受,这些MDB都使用容器管理事务(CMT)划分模型和事务型属性RequiresNew来部署。为了使这些设置可以生效,应当用事务型的QueueConnectionFactory(或TopicConnectionFactory)来配置MDB。在这样的设置中,消息消费过程可以用下面的步骤来描述:
 
·J2EE容器启动一个JTA事务。
·MDB所监听的队列/主题的XAResource被添加到事务中。
·从队列/主题消费一条消息,并传递给MDB的onMessage()方法。
·MDB处理消息。任何在MDB处理消息时会使用的事务型资源(比如:数据库、其它队列或主题以及JCA适配器)会被添加到步骤1中启动的同一个JTA事务中(当然,只有在资源支持XA时才可以)。
·如果处理成功(onMessage()调用无异常地返回),则J2EE容器会对JTA事务中的所有资源执行一次两阶段提交。因为JTA事务所拥有的资源之一是消费JMS消息的目的地,一旦JTA事务提交成功,消息就会从队列/主题中移除。
·如果处理失败(onMessage()调用抛出RuntimeException 异常),则J2EE容器会对表示步骤3中消费JMS消息的目的地的XAResource执行回滚,因此消息会停留在队列/主题中,稍后可能被重新发送。此外,在这种情况下,MDB实例会被销毁并从处理池中移除。
 
  如您所见,事务型消息消费的处理非常健壮。这样的处理模型保证了once-and-only-once的服务质量;换句话说,消息要么被成功地处理一次,要么根本不被处理(可能在超出预定义的重试次数或生存时间或者转移到停用(dead)消息队列之后,被从队列中移除)。
 
传统模型的缺点
 
  事务型消息消费模型尽管健壮且成功,但是它还是有一些严重的缺点。首先,分布式事务严重地影响了处理性能(当从本地事务切换到XA时,性能降低50%是不足为奇的)。
 
  第二个缺点是由CMT造成的,实际的事务提交发生在应用程序代码之外,在onMessage()调用返回之后。这也许没什么大不了的(毕竟CMT的整体思想是要将应用程序从事务处理中解脱出来),但是存在一些令人不愉快的问题——一些错误情况直到事务提交后才进行侦测。例如,在BEA WebLogic Server中,默认情况下,所有由处理CMP bean(创建,更新等等)引起的DML操作都被延迟到事务提交阶段。这意味着应用程序可以认为它成功地更新了CMP bean的一个实例,而实际上实际的SQL更新可能会因为违反数据库的某种约束而失败。最糟糕的是应用程序代码不能够对此做出反应或者只是恰当地进行记录,因为它看不到异常。
 
优化提议
 
  尽管对延迟的DML操作有一个应急方案(例如,在BEA WebLogic Server中,可以在部署描述符中禁用它),但是这伴随着性能损失。J2EE服务器将不能再聚集和/或批处理SQL更新(以便执行更高效),或者将它们全部忽略(如果事务稍后被标记为回滚的话)。
 
  本文认为,采用一种bean管理事务(bean-managed transaction,BMT)方法可以提供同样的服务质量,并对事务生命周期有更多的控制。应用程序代码将有机会恢复和/或更清楚地报告错误,同时避免上述的CMT模型的所有缺点。此外,我们预料从事务作用域中移除消息检索能带来重大的性能提升。
 
  在讨论BMT方法之前,我们需要分析在这种情况下从队列中消费消息会发生什么。如果我们使用BMT 划分部署MDB,J2EE服务器不会再把MDB监听的JSM目的地(队列或者主题)添加到事务中(事务将会在从队列中取走消息之后开始)。在这种情况下,BMT MDB应当用部署描述符中的非XA连接工厂配置;否则J2EE服务器会部署失败。
 
  根据JMS规范(JMS1.1第一节4.5.2),如果使用AUTO_ACKNOWLEDGE 或者 DUPS_OK_ACKNOWLEDGE模式非事务型地部署消息监听器,并且onMessage()方法抛出RuntimeException或它的任何子类,则消息会被重新发送。换句话说,重新设计用例来使用BMT是有可能的,如果在处理消息时出错的话,应用程序代码可以抛出RuntimeException,消息就会被重新发送(重试)。这种方法很有效,因为使用RuntimeException来表示不可恢复的错误是很自然的(例如Spring Framework的异常层次结构基本上全是基于RuntimeException的子类)。消息会重新发送,直到达到一定的次数(可在MOM软件层配置),之后它通常被丢弃或转移到停用消息队列,或者应用程序代码会计算消息被重新发送的次数,并决定什么时候应当停止尝试处理以及不处理就消费(如果适当的话,会产生错误消息)还是转移到另外的队列。
 
  如您所见,上述行为保证我们可以控制消息的重新发送,并且应用程序可以在处理反复失败的情况下重试。下面我们将展示如何在BMT情况下保证once-and-only-once行为。
 
非事务型消息消费模型
 
  让我们看看非事务型消息消费模型中的事件顺序:
 
  从队列/主题中消费一条消息,并传递给MDB的onMessage()方法
 
  应用程序启动一个BMT事务
 
  MDB处理消息。所有在MDB处理消息时会用到的事务型资源(例如:数据库、其他队列或主题,以及JCA适配器)都会被添加到步骤2中启动的BMT事务当中。应用程序全面控制BMT事务。它可以根据任何需要的业务逻辑提交或者回滚事务。最简单的情况是,如果从业务逻辑消息处理代码抛出任何异常,就回滚事务,否则就提交。如果调用onMessage()成功返回(没有抛出RuntimeException),则消息会被确认,并从队列中移除。
 
  您或许注意到了,从理论上来看,该序列有点简单了些,尽管其实现比普通的CMT情况要稍复杂。得到的回报是消息处理的巨大灵活性。例如,由应用程序代码来决定是提交还是回滚事务,并独立于消息确认。注意,直到onMessage()方法调用成功返回,消息才被确认。
 
Bean管理和容器管理的事务以及所要求的服务质量
 
  到现在为止,一切都还不错,但是让我们看看能否保证与使用事务型消息消费模型时相同的服务质量(once and only once)。我们需要分析两个问题:
 
·消息是否会因为消息处理而丢失
·消息是否会被多次处理
 
  第一条很简单。从我们的讨论来看,很明显,由于消息直到onMessage()成功返回才被确认,只需要保证应用程序代码没有在不恰当的地方屏蔽掉异常,并且在任何抛出异常的地方回滚了事务。这是一个非常简单自然的方法。
 
  第二条有些棘手。如我们所看到的,如果在业务处理过程中抛出异常,消息会被重新发送。如果我们遵循上面的范例就不成问题:如果有任何异常,则回滚事务,并进一步重新抛出异常给J2EE服务器。当然,如果错误真的不可恢复,就应当采取措施防止无限的消息发送。这可以在MOM层完成(例如,停用消息队列、重新发送限制等等),或者,如我们将演示的,可以在应用层处理。
 
  如果先前的处理尝试以事务回滚结束,则消息重新发送就不是问题。从应用程序的角度来说,下一次的发送尝试应当作为一条新的消息对待(同一条消息先前的处理尝试结果是不可见的,因为事务被回滚了),所以在这种情况下不会有什么问题,没有任何特别的处理规则。
 
  还要分析在不同组件发生灾难性故障的情况下会发生什么——例如,如果J2EE服务器崩溃——从我们要求的服务质量来看,系统在重启后其状态还一致么?发生在消息确认(出现在最后一步)之前的任何故障都会导致消息重新发送。因此我们只需要关心一种故障场景,也就是J2EE服务器或MOM(或者它们之间的连接)在代码成功完成处理并提交BMT事务之后、J2EE服务器确认消息之前出现故障。这会使系统处于不一致状态:从应用程序的角度看,消息被成功处理;但是从MOM的角度看,消息还没有成功发送(由于故障而没有被确认)。这会导致重复的消息发送和处理,从而违反了我们的服务质量条约。可以证明,由于事务提交和确认之间的时间间隔应该非常短,这种故障场景不会经常发生。尽管如此,如果系统处于高负载下,同时处理的消息没有几百个可能也有几十个,那么在建立恢复机制的时候就应当考虑这种情况。
 
  有必要再次强调,我们所讨论的是最严格的服务质量水平(once and only once),因而从如此少见的情况下的副本中恢复是必需的。如果您的用例可以使用更宽松的服务质量水平(例如,once or more)开发,就可以跳过下面描述的恢复机制。
 
  总而言之,当使用非事务型BMT消息检索时,为了保证once-and-only-once 服务质量,我们需要控制消息的重新发送行为(根据EJB规范,这通过从onMessage()方法抛出RuntimeException完成),还需要一些机制来防止在消息处理的特定阶段(在从onMessage()调用返回与消息确认之间)发生故障时处理重复消息。下面我们将讨论防止处理重复消息。
0
相关文章