UML软件工程组织

理解 JCA 事务
作者:Mikhail Genkin  出处:ibm.com.cn

电子商务的世界是一个飞速变化的环境。业务要求把现有企业信息系统(EIS)(比如客户信息控制系统(CICS)、信息管理系统(IMS)或 SAP)中的现有业务逻辑与数据集成在一起。关键的业务事务经常是用 Cobol 或 C 这样的过程性语言编写的。J2EE? 平台平台中有一个规范,该规范向开发人员提供了访问 EIS 事务和数据的接口,这个规范就是 J2EE 连接器架构(JCA)规范。

在本文中,我将解释 JCA 事务合约如何帮助电子商务应用程序实现事务性行为。具体地说,您将学习 JCA 的两个事务界定技术:分布式事务界定和程序性事务界定。我会解释每种技术各自的优劣,并提供一些技巧,来确定最适合您的应用开发场景。然后我会带您经历一个企业级编程示例,该示例实现了程序性事务界定。最后,文章总结了几个技巧,在选择事务界定解决方案和应用正确的部署描述符设置的时候,可以使用这些技巧。

任何与 J2EE 1.3 以及更高版本兼容的应用服务器,以及与 JCA 1.0 和 JCA 1.5 都兼容的资源适配器,都可以实现本文描述的技术。

JCA 事务概述

实际的示例会有助于展示一些常见的 JCA 事务问题。 在该例中,要完成的任务是要为一家公司构建电子商务应用程序,这家公司的主要业务是向客户销售工业制品。该公司决定构建一个 Web 站点,向更多的群体零售其产品。该 Web 站点允许任何使用浏览器的客户访问公司的主页;浏览可用产品的目录;查看详细的价格信息、可用性、可用项目的说明;向购物车中添加购买的产品;最后购买产品。为了举例说明必需的事务性行为,我把重点放在了客户决定购买商品的用例上。图 1 显示了该应用程序的设计。

图 1. 访问电子商务应用程序中的 EIS
 

 公司现有的 IT 基础设施是围绕两大企业信息系统构建的。EIS1 是主机系统,在 CICS 下面运行 Cobol 事务。运行在这个系统上的事务将实现业务逻辑,并且还要访问订单录入和客户关系管理(CRM)所需要的数据。EIS2 是一个 IMS 系统,它包含产品说明目录、价格信息和库存控制。要支持所要求的功能,J2EE 应用程序必须能够访问来自这两个系统的数据,并把它们无缝集成在一起。而要进行采购,则需要把以下操作作为一个工作单元(即作为一个事务)来执行:

  1. 取得当前产品价格(EIS2)。
  2. 把客户订单输入订单系统(EIS1)。
  3. 给客户记帐(EIS1)。
  4. 更新产品可用性信息(EIS2)。

步骤 2、步骤 3 或步骤 4 中的任何一个步骤的失败,都会造成取消前面所做的所有操作。

在该示例 Web 应用程序中,客户机(Web 层中的 JSP 页面)保存了一个对 CustomerSession 状态会话 EJB 组件的一个实例的引用,并调用该实例上的方法。可以使用 CustomerSession 来保存购物车的内容、从目录得到的产品信息以及客户信息。如图 1 所示,在与最终客户交互期间,会话 bean 搜集并保存了采购商品所必需的、特定于产品选择和客户的一些信息。在 CustomerSession 中定义的方法,将调用无状态会话 EJB 组件的方法(这个 bean 起到一个外观(facade)的作用),例如 OrderService,从而调用 EIS 上的事务。

在这个设计中,无状态会话 bean OrderService 充当着 EIS1 的外观。它定义了以下方法:

OrderInfo addOrder(String customerId, ItemInfo itemInfo) 用 JCA 通用客户机接口(CCI)调用 EIS1 上的 SHIPTO 事务。该事务查找客户的送货地址,准备 EIS 需要与送货部门沟通的信息。返回的结构 OrderInfo 包含订单 ID 号(用作跟踪)、送货成本和送货地址信息。

BillingInfo billCustomer(String customerId, OrderInfo orderInfo) 也使用 JCA CCI 调用 EIS1 上的 BILLTO 事务。该事务查询客户的信用卡号码,并借记客户的帐户,记下订单数量。返回的结构 BillingInfo 包含客户的总成本,包括运输费用和成本结构、该订单的 ID 号(用作跟踪和取消),以及客户信息。

void cancelOrder(OrderInfo orderInfo) 用 JCA CCI 调用运行在 EIS1 上的 RMVORD 事务来取消订单。

 会话 bean CatalogService(请参阅 图 1)充当 EIS2 的外观。它定义了以下方法:

double getItemPrice(String itemId) 用 JCA CCI 调用 EIS2 上的 ITMPRICE 事务。该事务将返回项目的最新价格信息。

void updateStockInfo(String itemId, int numItems) 用 JCA CCI 调用 EIS2 上的 UPDSTOCK 事务。这个事务根据项目 ID 号,更新当前的库存信息。输入参数 numItems 可以是正的,也可以是负的。
所有这 5 种方法和底层的 EIS 事务都运行在两个不同系统上,它们必须作为一个单一的业务事务的组成部分来执行。在下面一节中,您将看到如何实现这一点。

事务支持级别

不同的 EIS 有不同的事务性行为。有些 EIS 根本不支持事务。有些 EIS 支持事务,但是不支持双向提交(2PC)协议。这类事务被称为支持本地事务。有些 EIS 既支持本地事务,又支持 2PC。这类事务被称为支持分布式事务,或者全局事务。全局事务也被称为 XA 事务,因为它们包含 XAResource 接口。

清单 1 显示了 CICSECI 资源适配器的 ra.xml 中的一小段,您要用这个适配器访问 EIS1。 <transaction-support> 元素的 Localtransaction 值表明,这个资源适配器支持本地事务,但是不能参与全局事务。会话 bean OrderService 用这个资源适配器来访问包含 EIS1 的 CRM 事务。

 清单 1. CICSECI 资源适配器的 ra.xml 描述符的代码片段

<!DOCTYPE connector PUBLIC "-//Sun Microsystems, Inc.//DTD Connector 1.0//EN"
"http://java.sun.com/dtd/connector_1_0.dtd">

<connector>
<display-name>ECIResourceAdapter</display-name>
<description>CICS J2EE ECI Resource Adapter</description>
<vendor-name>IBM</vendor-name>
<spec-version>1.0</spec-version>
<eis-type>CICS</eis-type>
<version>5.0.0 </version>
<license>
<description> </description>
<license-required>true</license-required>
</license>
<resourceadapter>
<managedconnectionfactory-class>
com.ibm.connector2.cics.ECIManagedConnectionFactory</managedconnectionfactory-class>
<connectionfactory-interface>
javax.resource.cci.ConnectionFactory</connectionfactory-interface>
<connectionfactory-impl-class>
com.ibm.connector2.cics.ECIConnectionFactory</connectionfactory-impl-class>
<connection-interface>javax.resource.cci.Connection</connection-interface>
<connection-impl-class>com.ibm.connector2.cics.ECIConnection</connection-impl-class>
<transaction-support>LocalTransaction</transaction-support>

清单 2 显示了 IMS 资源适配器的 ra.xml 的代码片断,我用该适配器访问 EIS 2。在该例中, <transaction-support> 元素的值 XATransaction 表示资源适配器支持全局事务或分布式事务。

清单 2. IMS 资源适配器的 ra.xml 描述符的代码片断

<!DOCTYPE connector PUBLIC "-//Sun Microsystems, Inc.//DTD Connector 1.0//EN"
"http://java.sun.com/dtd/connector_1_0.dtd">

<connector>
<display-name>IMS Connector for Java</display-name>
<description>J2EE Connector Architecture resource adapter for IMS
accessing IMS transactions using IMS Connect </description>
<vendor-name>IBM Corporation</vendor-name>
<spec-version>1.0</spec-version>
<eis-type>IMS</eis-type>
<version>1.2.6</version>
<license>
<description>IMS Connector for Java is a component of the IMS Connect product and as such
is not separately licensed. It requires an IMS Connect license.</description>
<license-required>true</license-required>
</license>
<resourceadapter>
<managedconnectionfactory-class>
com.ibm.connector2.ims.ico.IMSManagedConnectionFactory</managedconnectionfactory-class>
<connectionfactory-interface>
javax.resource.cci.ConnectionFactory</connectionfactory-interface>
<connectionfactory-impl-class>
com.ibm.connector2.ims.ico.IMSConnectionFactory</connectionfactory-impl-class>
<connection-interface>
javax.resource.cci.Connection</connection-interface>
<connection-impl-class>
com.ibm.connector2.ims.ico.IMSConnection</connection-impl-class>
<transaction-support>XATransaction</transaction-support>
<config-property>

JCA 事务支持

清单 1 和清单 2 中所示的资源适配器在其事务支持上有所区别。但是,在我的电子交易应用程序中,我需要把两个不同系统上的事务协调起来。为了有助于实现这一点,JCA 事务合约定义了一套框架性接口,通过这套接口,应用程序服务器和 EIS 可以协调事务。EJB 容器通过资源适配器实现的 ManagedConnection 接口与 EIS 对话,该接口表示了与 EIS 的实际连接。 ManagedConnection 的实现通常要使用专有的库,用 EIS 能够理解的协议与 EIS 进行对话。

如果资源适配器支持本地事务,那么它还要实现 javax.resource.spi.LocalTransaction 接口。当 EJB 容器需要初始化事务的时候,它要调用 ManagedConnection 实现的实例上的 getLocalConnection() 方法。然后,它要调用 Localtransactionbegin()、 commit() 和 rollback() 方法来控制事务。

如果资源适配器支持 XA 或全局事务,那么它还要实现 javax.transaction.xa.XAResource 接口。每个符合 J2EE 规范的应用程序服务器都有一个叫作事务管理器的内部组件。事务管理器实际是 javax.transaction.TransactionManager 接口的实现。这个组件帮助应用程序服务器管理事务的边界。事务管理器用 ManagedConnection 接口定义的 getXAResource() 方法得到 XAResource 的实例。事务管理器用 XAResource 接口定义的方法,在多个资源管理器之间协调 2PC 协议。

事务界定策略

JCA 提供了两个处理事务的选项:程序性事务界定或声明性事务界定。第一个选项要求您使用 Java 事务 API(JTA),显式地为每个事务的 begin、 commit 和 rollback 操作编写代码。在这种情况下,事务界定代码与实现业务逻辑的代码混杂在一起。

第二种方法是声明性事务界定,它不包含任何额外的编码工作。如果选择这种方法,那么 EJB 部署人员需要修改 bean 的部署描述符的设置,配置事务性行为。这样,EJB 容器就会用这些部署描述符设置在合适的点上自动 begin、 commit 或 rollback 事务。在这种情况下,在 EJB 组件中实现的业务逻辑可以保持可移植性,而且不需要重写 bean 实现,就能调整事务性行为。

 在多数情况下,声明性事务界定是首选选项。程序性界定通常用在声明性事务界定不够灵活的情况下。在我的示例中,两个 EIS 和它们对应的资源适配器有不同的事务支持级别。如果这些是惟一需要考虑的因素,那么使用 2PC 协议的分布式事务应当是保证数据一致性和完整性的最佳方法。

但是,为了让事情更有趣,在我的示例中,只有 EIS2 支持分布式事务。为了使用 2PC,包含在事务中的所有系统都必须支持它。所以,我不能使用分布式事务,不得依靠本地事务才能保证在两个系统之间更新的一致性。我需要对每个 EIS 的访问进行分组,而且如果两个 EIS 的其中之一失败,那么还需要手工取消对另一个 EIS 的修改。用来取消前面提交的变化的事务通常叫 补偿事务。我将用程序性事务界定为手动更新提供更大的灵活性。

程序性事务界定

清单 3 显示了会话 bean CustomerSession 中定义的方法 placeOrder 在应用程序中的实现。该方法用 JTA API 启动控制 EIS1 和 EIS2 的访问的本地事务。在 placeOrder 方法中,首先要访问 EIS2,以获得最新的价格信息。虽然在性质上,这个访问是只读的,但它仍然应当发生在事务的范围中。这是因为,对于 EIS2 中价格信息的更新,可能是由其他应用程序持续进行的,而您需要保证看到的是一致提交的价格数据。请注意,出于简便的原因,例外处理和业务逻辑已经做了简化处理。

清单 3. CustomerSession EJB 的 placeOrder() 方法(为了简便起见,对错误处理进行了简化)

public OrderInfo placeOrder(ItemInfo itemInfo, CustomerInfo custInfo)
throws OrderException {

// Get a reference to the UserTransaction.
// Initialize variables.
BillingInfo billingInfo = null;
OrderInfo orderInfo = null;
double itemPrice = 0.0;

UserTransaction ut = null;

try
{
InitialContext ic = new InitialContext();
ut = (UserTransaction)ic.lookup("jta/UserTransaction");
}
catch (Exception e)
{
throw new OrderException(e.getMessage());
}

// Look up latest pricing information in EIS2 including customer discount.
try
{
ut.begin();
itemPrice = catalogService.getItemPrice(itemInfo.getItemId(),
custInfo.getCustomerId());
ut.commit();
}
catch ( Exception e)
{
try
{
ut.rollback();
}
catch (Exception ex)
{
// Rollback failed.
// Log the error here, nothing to recover.
}

// Throw exception back to the UI tier, nothing to compensate yet.
throw new OrderException(e.getMessage());
}

itemInfo.setItemPrice(itemPrice);

// Update EIS1 - local transaction
try
{
ut.begin();
billingInfo = orderService.billCustomer( custInfo.getId(), itemInfo );
orderInfo = orderService.addOrder( custInfo.getId(), itemInfo );
ut.commit();
}
catch ( Exception e)
{
// Nothing to compensate in EIS2 yet.

try
{
ut.rollback();
}
catch (Exception ex)
{
// Rollback failed -- log the error.
// Additional checks and error handling to ensure consistency in EIS1.
}

throw new OrderException(e.getMessage());

}

// Update EIS2.
try
{
ut.begin();
catalogService.updateStockInfo(orderInfo.getItemId(), orderInfo.getItemNumber());
ut.commit();
}
catch( Exception e )
{

// Roll back the original transaction to EIS2.
try
{
ut.rollback();
}
catch (Exception ex)
{
// Rollback failed - log the error.
// Additional checks and error handling.
// Do not exit the method yet.
}

// Compensate changes to EIS1 as a single one-phase transaction.
try
{
ut.begin();
orderService.cancelOrder( orderInfo );
orderService.cancelCharge( billingInfo );
ut.commit();
}
catch ( Exception ex)
{
// Compensation failed, log error
try
{
ut.rollback();
}
catch (Exception exx)
{
// Rollback failed
// Log error
}

throw new OrderException(ex.getMessage());

}

// Throw exception back to the UI tier
throw new OrderException(e.getMessage());

}

return orderInfo;

}

下一步是执行对 EIS1 的两个更新:一个是对订单系统更新,另一个是对记帐系统进行更新。可以在本地事务的范围内执行这些更新。如果其中一个更新失败,那么可以向 UI 层重新抛出异常,退出该方法,不再更新 EIS2。UI 层则需要告诉用户事务失败了,并请求用户指示如何处理情况。用户可能选择重试或者退出应用程序。因为这两项操作都是运行在同一事务中,而前面对 EIS2 的访问是只读的,所以在这一段中需要补偿事务。

您要做的最后一件事,就是更新 EIS2 中的可用库存数据。这项操作还是在本地事务的范围中执行的,但是,本地事务是与已经完成的 EIS1 事务不同的事务。因为要使用程序性事务界定,所以更新 EIS2 操作的失败,会造成取消 EIS1 中已经提交的修改。这就是 catch 块中包含对 EIS1 事务的调用的原因,为了取消对订单和记帐系统的更新。

值得注意的是,对于 2PC 的分布式事务,程序性事务界定的方法不是完美的替代品。如果补偿事务本身失败,那么系统就会处于不一致状态。在现实的应用程序中,需要额外的异常处理来处理这类情况。例如,在补偿事务失败的情况下,您可以向系统管理员发送通知,或者用消息技术稍后重试这些事务。

EJB 部署描述符设置

在基于 EJB 的解决方案中,EJB 部署人员必须通过配置 EJB 部署描述符设置,告诉应用程序服务器如何处理事务界定。为应用程序选择正确的部署描述符设置的第一步,就是评估它的需求。在这里,该需求是示例 JCA 实现的需求:

EJB 组件 CustomerSession 的部署描述符必须指示您,您将用通过编程实现的(也就是由 bean 管理的)事务界定 ( TX_BEAN_MANAGED)。会话 bean OrderService 和 CatalogService 的部署描述符应当使用声明性(也就是由容器管理的界定)事务界定,这些 bean 上的方法应当一直在事务的上下文环境内执行。

 会话 ben OrderService 和 CatalogService 提供对底层 EIS 事务的访问。这些 bean 上的方法由会话 bean CustomerSession 调用,它们实现了应用程序背后的处理逻辑,其他面向处理的组件也可以调用它们。

CustomerSession bean 应当在调用 OrderService 上的方法之前启动一个事务,因为要由这个事务来包装在 EIS1 上进行的多个操作。因为要在 EIS2 上进行两个操作,而且每一个操作都可以作为独立的事务执行(而且无论哪个操作都不能作为包装所有 4 个操作的全局操作的一部分), 所以不管是程序性界定,还是声明性界定,都可以用来调用 CatalogService 方法。(请注意,我倾向于由 CustomerSession bean 负责为所有所操作控制事务边界。)

 根据这些需求,可以为 CatalogService 和 OrderService bean 使用 TX_REQUIRED 部署描述符设置。该设置可以保证 bean 的方法可以加入已经进行的事务中,或者在需要的时候启动新的事务。请注意,使用 TX_REQUIRED 时,如果调用代码没有启动事务,那么每个调用,包括对 EIS 的那些调用,都将作为独立的事务发生,而这可能并不是您想要的。

一个替代的办法可能是强制所有实现处理逻辑的组件(例如, CustomerSession bean) 执行事务界定。通过对会话 bean OrderService 和 CatalogService 使用 TX_MANDATORY 部署描述符,可以实现这个方法,它能保证应用程序执行预期的事务性行为。如果选择这种方法,那么在调用组件时,需要在调用 EIS 代理的方法之前启动一个事务,否则就会抛出异常。这种方法的不足之处是,它要求只需要单一方法事务的组件也要负责控制事务的边界。

指定事务隔离级别

J2EE 规范定义了用来为具体 EJB 方法指定事务隔离级别的部署描述符属性。例如,EJB 部署人员可以设置会话 bean CatalogService 的方法 getItemPrice() 的事务隔离级别,使其在 TRANSACTION_READ_COMMITTED 隔离级别中运行,以确保在事务期间,只读取提交的价格数据。通过修改隔离级别,J2EE 开发人员或部署人员可以在性能的约束下平衡数据完整性,从而对应用程序进行调整。

但是,不同的 EIS 有不同的事务性能力,因此会以不同的方式对事务隔离设置作出反应。在这种情况下,改变 EIS 代理的事务隔离设置,可能不会有什么区别,因为 ECI 和 XCF 通信协议不会把该信息传递到后端。运行在这些系统上的 COBOL 事务会用这些平台上特定的技术解决事务性隔离问题。

选择解决方案

一般来说,要确定具体目标 EIS 会如何响应事务隔离设置,需要参考该 EIS 的文档,对于用来提供连接性的资源适配器,也是如此。虽然示例中使用的资源适配器不响应 J2EE 事务性隔离级别设置的变化,但是某些围绕关系数据库系统构建的 EIS 确实可能会受到影响。重要的是,要理解由于这些变化在具体 EIS 中可能带来的正确行为,因为这对 EIS 和应用程序的性能指标可能会有严重冲击。在更坏的情况下,底层数据的完整性可能被破坏。进行 J2EE 项目的开发人员应当咨询 EIS 管理员,以确保事务性管理隔离遵循的是正确的策略。

结束语

Java 连接器架构规范为 J2EE 开发人员构建包含传统系统的事务性 J2EE 程序提供了方便的解决方案。JCA 允许在集成现有的 EIS 系统的同时,维持电子商务所需要的正确的事务性语义。

您通常需要解决的关键问题是:不得不处理不同的资源适配器,提供不同级别事务支持。在许多情况下,有可能无法依赖底层的事务管理器,在受影响的系统之间协调分布式事务。在有些情况下,即使有可能,分布式事务也可能由于与双向提交协议有关的性能问题而变得不适用。

在这种情况下,可能需要依赖补偿事务逻辑和程序性事务界定。这种方法也有它的不足之处。补偿事务本身可能会失败,给系统留下一个不一致的状态。

另一个需要注意的问题是,JCA 规范没有规定资源适配器应当如何处理 EJB 事务隔离级别,必须解决这个问题。有些资源适配器只是忽略这些设置,因为目标 EIS 的具体通信协议不能把这个信息传递给后端,或者是因为目标 EIS 的事务模型不提供等价的概念。在其他情况下,对 J2EE 事务隔离级别的修改可能会严重影响 EIS 的性能。为了确保正确操作,您应当仔细研究资源适配器的文档,如果这方面的内容介绍得不够详细,那么您还应该咨询厂家。


版权所有:UML软件工程组织