事务界定策略
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() 方法(为了简便起见,对错误处理进行了简化)
2 throws OrderException {
3 // Get a reference to the UserTransaction.
4 // Initialize variables.
5 BillingInfo billingInfo = null;
6 OrderInfo orderInfo = null;
7 double itemPrice = 0.0;
8 UserTransaction ut = null;
9 try
10 {
11 InitialContext ic = new InitialContext();
12 ut = (UserTransaction)ic.lookup("jta/UserTransaction");
13 }
14 catch (Exception e)
15 {
16 throw new OrderException(e.getMessage());
17 }
18 // Look up latest pricing information in EIS2 including customer discount.
19 try
20 {
21 ut.begin();
22 itemPrice = catalogService.getItemPrice(itemInfo.getItemId(),
23 custInfo.getCustomerId());
24 ut.commit();
25 }
26 catch ( Exception e)
27 {
28 try
29 {
30 ut.rollback();
31 }
32 catch (Exception ex)
33 {
34 // Rollback failed.
35 // Log the error here, nothing to recover.
36 }
37 // Throw exception back to the UI tier, nothing to compensate yet.
38 throw new OrderException(e.getMessage());
39 }
40 itemInfo.setItemPrice(itemPrice);
41 // Update EIS1 - local transaction
42 try
43 {
44 ut.begin();
45 billingInfo = orderService.billCustomer( custInfo.getId(), itemInfo );
46 orderInfo = orderService.addOrder( custInfo.getId(), itemInfo );
47 ut.commit();
48 }
49 catch ( Exception e)
50 {
51 // Nothing to compensate in EIS2 yet.
52
53 try
54 {
55 ut.rollback();
56 }
57 catch (Exception ex)
58 {
59 // Rollback failed -- log the error.
60 // Additional checks and error handling to ensure consistency in EIS1.
61 }
62 throw new OrderException(e.getMessage());
63
64 }
65 // Update EIS2.
66 try
67 {
68 ut.begin();
69 catalogService.updateStockInfo(orderInfo.getItemId(), orderInfo.getItemNumber());
70 ut.commit();
71 }
72 catch( Exception e )
73 {
74 // Roll back the original transaction to EIS2.
75 try
76 {
77 ut.rollback();
78 }
79 catch (Exception ex)
80 {
81 // Rollback failed - log the error.
82 // Additional checks and error handling.
83 // Do not exit the method yet.
84 }
85 // Compensate changes to EIS1 as a single one-phase transaction.
86 try
87 {
88 ut.begin();
89 orderService.cancelOrder( orderInfo );
90 orderService.cancelCharge( billingInfo );
91 ut.commit();
92 }
93 catch ( Exception ex)
94 {
95 // Compensation failed, log error
96 try
97 {
98 ut.rollback();
99 }
100 catch (Exception exx)
101 {
102 // Rollback failed
103 // Log error
104 }
105 throw new OrderException(ex.getMessage());
106 }
107 // Throw exception back to the UI tier
108 throw new OrderException(e.getMessage());
109 }
110 return orderInfo;
111 }
112
113
下一步是执行对 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 的性能。为了确保正确操作,您应当仔细研究资源适配器的文档,如果这方面的内容介绍得不够详细,那么您还应该咨询厂家。