事务策略: 高性能策略

事务需要确保高度的数据完整性和一致性。但是事务的开销也很大;它们会消耗宝贵的资源并且会减慢应用程序的速度。当正在使用一个以毫秒计的高速应用程序时,可以通过实现高性能事务策略在某种程度上维护ACID(原子性、一致性、隔离和持久性)属性。如将在本文中看到的,高性能策略并不像其他事务策略一样健壮,并且它不是所有涉及到高性能应用程序的用例的最佳选择。但是确实有的时候这个策略可以帮助您维持最快速的处理时间,同时支持某种程度的数据完整性和一致性。

本地事务和补偿框架(Compensation Framework)

EJB 3 中的本地事务
要将本地事务与 EJB 3 会话 bean 一起使用,可以在会话 bean 开始处使用 @TransactionManagement(TransactionManagementType.BEAN) 注释,以便告诉容器不要管理事务。此注释防止容器的事务管理器对事务处理进行控制。

从数据持久化的角度看,进行数据库更新操作的最快速的方式是将本地事务 与数据库存储过程一起使用。本地事务(有时称为数据库事务)是由数据库而不是容器环境管理的事务。不需要在应用程序中编写任何事务逻辑(比如 Spring 中的 @Transactional 注释或 EJB 3 中的 @TransactionAttribute 注释)。

存储过程速度较快,因为它们是预先编译好的并且驻留在数据库服务器上。对于高性能策略来说,它们不是必需的,并且它们的效率和性能有时还存在一些有趣的争议(见 参考资料中的 “So, are Database Stored Procedures Good or Bad?”链接)。使用存储过程会降低应用程序的可移植性,增加复杂度并降低总体的敏捷度。但是它们通常比基于 Java 的 JDBC SQL语句的速度快,并且当性能比可移植性和维护更重要时,它们是不错的选择。这就是说,如果需要的话,可以将任何基于 JDBC 的框架与纯 SQL一起使用。

既然本地事务这么快,那么为什么不是每个人都使用它呢?主要原因是除非使用像 连接传递这样的技术,否则您不能维护传统的 ACID事务属性。使用本地事务,数据库更新操作会被视为单个的工作单元而不是一个整体。本地事务的另一个限制是不能将它们与传统的关系对象映射 (ORM)框架,比如 Hibernate、TopLink 或 OpenJPA,一起使用。仅限于使用基于 JDBC 的框架,比如 iBATIS 或Spring JDBC(请参见 参考资料),或者您自己开发的数据访问对象 (DAO) 框架。

连接传递(Connection Passing)

连接传递是一个技术,在缺少健壮的基于容器的事务管理器时,可以将 Connection 对象上的 autocommit 标志设置为 false 并在方法调用之间传递数据库连接。一旦连接完成,您就可以在 Connection 对象上执行 commit() 来提交变更,或者执行 rollback() 来收回变更。使用连接传递通常来说是个坏主意,主要因为您试图要做基于容器的事务管理器做的内容,只是效率更低、更易犯错而已。如果发现正在使用连接传递,那么应该切换到编程式或声明式的事务模型。

高性能策略基于本地事务的使用。但是等等 — 如果本地事务模型不支持基本的 ACID 属性,那么基于这种模型的事务策略如何能成为一个好策略呢?答案是高性能策略利用本地事务模型,配合补偿框架 一起使用。每个单个的数据库更新操作都是独立存在的,但是在发生错误时,补偿框架会确保保留各个事务,从而维护原子性和一致性。

图 1 展示了不使用补偿框架只使用本地事务会发生什么。注意,第三个存储过程失败时,逻辑工作单元 (LUW) 结束,数据库处于不一致的状态,只应用了三个更新中的两个:


图 1. 没有补偿框架的本地事务
没有补偿框架的本地事务

补偿框架就位后,发生错误时,成功的事务会被保留,这样会维持一致的数据库。图 2 展示了发生与图 1 中的错误一样的错误时会发生怎样的情况(理论上)。注意,一旦存储过程成功返回,它就会使用补偿框架注册。第三个存储过程失败时,它会触发一个事件,告诉补偿框架反转此补偿范围 中包含的一切。


图 2. 带有补偿框架的本地事务
带有补偿框架的本地事务

这种技术通常称为放松 ACID(RelaxedACID)。这是一个典型的事务解决方案,适用于长期运行的事务,这些事务在面向服务的架构中将业务流程执行语言(Business ProcessExecution Language,BPEL)用于流程编排或用于使用 Web服务。在这种情况下,一个事务工作单元可能花费几分钟、几小时,甚至几天来完成。假设您能够如此长时间的锁定资源是很不现实的。此外,也很难(有时几乎不可能)在某些异构平台或 HTTP 之上(像在使用 Web 服务情况下)传播事务。

放松 ACID的概念也可以应用于短期运行的事务。在使用高性能事务策略的情况下,事务的持续时间以秒(而不是分钟)来计算。然而,同样的原则也适用于 —需要最大化数据库并发性且最小化等待时间和处理时间的极端高性能情境。而且,希望利用最快速的可行方式进行数据库更新操作。这是通过使用本地事务和数据库存储过程实现的。补偿框架只是在发生错误时进行协助;一切正常时它不会出现。数据库并发性位于其最大值,数据库更新操作会以可能达到的最快的速度进行,在发生错误时,补偿框架会为您处理一切。听上去很完美,是吧?但是,遗憾地是事实并非如此。

 



回页首

 

折衷与问题

很多折衷和问题都与这个事务策略相关。记住,它为您提供了最快速执行事务的方式并在某种 程度上仍然保持 ACID 属性。您放弃了事务隔离、健壮性和简单性。只有在使用本系列介绍的其他三个策略了时不能获得您想要的性能时才应使用此策略。补偿框架复杂而且有风险,无论是自己构建的还是开源或商业解决方案提供的(在本文稍后部分我会介绍一些)。

这个策略最大的问题是缺少健壮性和数据一致性,大部分基于补偿的解决方案都是这样。因为没有事务隔离,每个数据库更新操作都被视为单个的工作单元。因此,另一个工作单元可能会对正在处理中的数据进行操作。如果在 LUW执行中发生错误,此时反转更新可能为时已晚;或者更常见的是,反转更新会导致级联问题。例如,假设您正在处理一个很大的订单,要耗尽该项的库存。在处理期间,会触发一个事件(因为在 LUW执行期间该订单提交到了数据库),自动向供应商发送消息补充该项的库存。如果在订单处理过程中发生错误,补偿框架会反转事务,但是订单的影响(也就是那个补充库存的消息)已经被发送到了供应商,导致某个项的库存过剩。如果保持了传统的 ACID 属性,那么补充库存的事件消息在订单流程的整个 LUW完成之前是不会被发送出去的。这是众多例子中的一个,仅此说明数据的不一致是如何发生的,即使使用了补偿框架来维持事务原子性。

某些业务情境或技术不对高并发性事务策略开放。特别是,异步处理场景在使用补偿框架和放松 ACID时会面临较大的风险。在某些情况下,您可能需要牺牲一些性能,使用较慢的基于 ACID 的事务策略。此策略的另一个弊端是不能使用基于 ORM的持久性框架,该框架需要编程式或声明式事务模型。您并未局限到只能使用原始 JDBC 编写;您可以使用大量基于 JDBC 的框架,包括iBATIS(开源 SQL 映射框架)和 Spring JDBC。或者您也可以使用自己的自定义基于 DAO 的框架。ORM限制可能要求您接受另一个弊端 — 可维护性和通过事务支持获得更好性能的技术选择。

尽管通过使用补偿框架可以维护某种程度的数据库一致性,但是这个策略也会带来高度的风险。在事务反转的过程中可能会发生错误,使您的数据库处于不一致的状态。在本例中,某些数据库更新操作可能会被反转,而另外一些不会,有时需要手动干预修复这一问题。因此,在单个 LUW中几乎没有数据库更新操作的应用程序适合使用这种事务策略。此外,使用此策略的应用程序通常在交错的 LUW中没有共享的实体使用,这也就意味着很少出现多个用户同时操作同一实体(比如帐户、客户或订单)的情况。这些特点降低了由于不一致性和缺少事务隔离而导致灾难性结果的机会。

适合这种特殊事务策略的应用程序都比较健壮,很少发生错误(错误率小于10%)。执行事务补偿是一项开销很大且耗时的操作。如果经常反转数据库更新操作,系统速度会减慢而且与使用其他事务策略相比,这个策略可能会导致整体性能下降。要进行的补偿性更新越多,数据库不一致的风险越大。确保在选择该事务策略之前分析错误率。

本文剩下的部分将介绍现有的补偿框架并展示一个使用自定义解决方案的简单实现来阐释我前面介绍的概念。

 



回页首

 

现有补偿框架

同样的概念,不同的问题

补偿框架通常与长期运行的事务相关联(有时称为 L-R)。它们通常会在业务流程服务器(比如 Microsoft BizTalkServer、Oracle WebLogic Integration、Oracle BPEL Process Manager 和 IBMWebSphere Process Server)上找到。补偿框架也可用作解决方案来解决 Web服务空间中与事务相关的挑战。遗憾地是,这些框架不适合实现高性能事务策略,也就是说它们适合需要协调的短期解耦的活动,而不是长期运行的事务。

Java 平台中可用的几个补偿框架:J2EE Activity Service for Extended Transactions (JSR 95) 和 JBoss Business Activity Framework(请参见 参考资料)。它们提供了注册、消息传递和补偿触发器逻辑(不是更新反转方法本身)。像侧栏 同样的概念,不同的问题 中介绍的几个补偿框架一样,这些框架通常与长期运行的事务或基于 Web 的请求相关,并且很难与高性能事务策略一起使用。因此,使用这种事务策略时,很可能发现要创建自己的自定义补偿框架。

尽管 J2EE Activity Service 规范主要针对应用服务器厂商,也可以将同样的概念应用到您自己的自定义补偿框架。因此,在本部分中我会为您简单介绍一下 J2EE Activity Service,使您了解补偿框架是如何运作的。

J2EE Activity Service for Extended Transactions 是基于 OMG Activity Service 的(请参见 参考资料)。J2EE Activity Service 规范定义了一组接口和类,它们会协调和控制活动 内操作的执行。活动是一组注册的操作(比如数据库更新操作)。活动由控制器 控制和协调,控制器是一个可插入协议,通常作为第三方插件组件实现。每个活动都包含一个信号集javax.activity.SignalSet),它会向每个注册的操作发送信号javax.activity.Signal)。图 3 展示了使用补偿时发生情况的概念视图:


图 3. J2EE Activity Service 概念视图
JSR-95 概念视图

活动必须注册控制器(或者更具体地说是控制器内的补偿管理器组件)。活动完成后,会发送信号(在本例中是 SUCCESS 或FAILURE)给控制器。如果控制器收到了来自活动的 SUCCESS 信号,它会发送信号到协调器组件(在本例中是PROPAGATE),从而调用下一个活动。注意 图 3 的步骤 8 中 FAILURE 信号被发送回控制器中。在本例中,控制器会将 FAILURE 信号发送给协调器,从而以反转顺序调用补偿活动。尽管未在图 3 中表示出来,但是控制器还会监控补偿活动和协调器之间往返的信号以确保成功完成反转活动。

 



回页首

 

实现自定义补偿框架

编写自定义补偿框架似乎令人望而生畏,但实际上它并不十分复杂 — 只是很耗时。可以使用普通 Java 代码或使用更高级的技术实现您自己的补偿框架。本着追求简洁的精神,我将为您展示一个使用纯 Java 代码的简单示例,使您能了解这一概念;将创造的乐趣留给您。

无论使用开源、商业还是自定义补偿框架,您都必须提供可以调用的方法、SQL或存储过程来反转数据库更新操作。这也是我喜欢将存储过程用于高性能策略的另一个原因。它相对容易编目,并且是独立的,它们使得识别(和执行)补偿过程很容易。所以我要在下面展示的示例中使用存储过程。

这里不再赘述不必要的细节,假设数据库中已经有了以下准备运行的存储过程:

  • sp_insert_trade(向数据库中插入新的库存交易订单)
  • sp_insert_trade_comp(通过在数据库中执行删除,反转交易插入)
  • sp_update_acct(更新帐户余额,反应库存的买入或售出)
  • sp_update_acct_comp(更新帐户余额到最新更新的值)
  • sp_get_acct(从数据库中获取帐户)

这里还跳过了使用 CallableStatement JDBC 代码的 DAO 类,这样可以将精力集中到与此策略关系最密切的代码。(有关使用直接的 JDBC 调用存储过程的引用和代码的相关信息,请参见 参考资料)。因为自定义补偿协调器的实现变化很大而且可能相当冗长,我只为您展示底层结构并提供关于如何填补剩余的实现代码的说明。

根据您实现策略的方式以及用于补偿更新的技术,用于控制更新反转操作的注释或逻辑可能在应用程序的 API 层或其 DAO层。要解释实现高性能策略的技术,我将使用一个简单的库存交易示例,其中协调补偿范围的逻辑位于应用程序的 API层。在本例中,与库存交易相关的两个活动是向数据库中插入库存交易(活动 1)和更新帐户余额以反应库存交易(活动2)。两个活动分别使用本地事务和存储过程的方法实现。自定义补偿协调器(CompController)负责管理补偿范围和在发生错误时反转活动。

清单 1 阐释了库存交易方法,没有 使用补偿事务。processTrade() 引用的 AcctDAOTradeDAO 类包含 JDBC 逻辑,用于执行我前面列出的存储过程。简单起见,这里将跳过这些类。


清单 1. 没有补偿事务的库存交易示例

				


public class TradingService {





   private AcctDAO acctDao = new AcctDAO();


   private TradeDAO tradeDao = new TradeDAO();





   public void processTrade(TradeData trade) throws Exception {





      try {


         //adjust the account balance


         AcctData acct = acctDao.getAcct(trade.getAcctId());


         if (trade.getSide().equals("BUY")) {


            acct.setBalance(acct.getBalance()


               - (trade.getShares() * trade.getPrice()));


          } else {


            acct.setBalance(acct.getBalance()


               + (trade.getShares() * trade.getPrice()));


          }





          //insert the trade and update the account


          long tradeId = tradeDao.insertTrade(trade);


          acctDao.updateAcct(acct);





      } catch (Exception up) {


         throw up;


      }


   }


}


 

注意 清单 1 缺乏事务管理(没有编程式或声明式事务注释或代码)。如果在执行 updateAcct() 方法期间发生错误,insertTrade() 方法插入的交易将不会回滚,导致数据库不一致。尽管这个代码速度快,但是它不支持 ACID 事务属性。

要应用高性能事务策略,实现要创建(或使用)补偿框架来跟踪活动并在发生错误时反转它们。清单 2 展示了自定义补偿协调器的一个简单示例,概述了创建您自己的自定义补偿框架所需的步骤:


清单 2. 自定义补偿框架示例

				


public class CompController {





   //contains activities and the callback method to reverse the activity


   private Map compensationMap;





   //contains a list of active compensation scopes and activity sequence numbers


   private Map compensationScope;





   public CompController() {


      //load compensation map containing callback classes and methods


   }





   public void startScope(String compId) {


      //send jms start message containing compId as JMSXGroupId


   }





   public void registerAction(String compId, String action, Object data) {


      //send jms data message containing sequence number and data


      //using compId as JMSXGroupID.


      //CompController manages sequence number internally using the


      //compensationScope buffer and stores in JMSXGroupSeq message field


   }





   public void stopScope(String compId) {


      //consume and remove all messages having compId as the JMSXGroupID


      //without taking action


      //remove compId entries from compensationScope buffer


   }





   public void compensate(String compId) {


      //consume all messages having compId as the JMSXGroupID and process in


      //reverse order


      //using the compensation map and reflection to invoke reversal methods


      //remove compId entries from compensationScope buffer


   }


}


 

compensationMap 属性包含预先加载的所有活动的列表(按名称排列)以及相应的反转活动的类和方法(按名称排列)。本示例的内容可能包含以下条目:{"insertTrade", "TradeDAO.insertTradeComp"}{"updateAcct", "AcctDAO.updateAcctComp"}compensationScope 属性包含按 compId 排列的活动的补偿范围列表以及到目前为止注册的活动的列表。此缓冲区用于获取 registerAction() 方法使用的下一个序列号。其余方法基本是不言自明的。

注意,补偿协调器实现使用 Java 消息服务(JMS)传递消息。选择这种技术主要因为它提供了一种方式(通过使用持久消息和有保证的交付)确保补偿期间发生故障时,不能回滚的事务仍在 JMS队列中并且可以被另一个线程拾取和执行。JMS消息传递还允许异步活动注册和补偿处理,进一步加速应用程序源代码。当然,将补偿信息保留在内存中会极大地加速处理,但是如果补偿协调器发生故障,则会进一步导致数据库不一致。

清单 3 中的源代码示例阐释了将自定义补偿框架应用到 清单 1 中的原始应用程序源代码的技术:


清单 3. 带有补偿框架的库存交易示例

				


public class TradingService {





   private CompController compController = new CompController();


   private AcctDAO acctDao = new AcctDAO();


   private TradeDAO tradeDao = new TradeDAO();





   public void processTrade(TradeData trade) throws Exception {





      String compId = UUID.randomUUID().toString();


      try {


         //start the compensation scope


         compController.startScope(compId);







         //get the original account values and set the acct balance


         AcctData acct = acctDao.getAcct(trade.getAcctId());


         double oldBalance = acct.getBalance();




         if (trade.getSide().equals("BUY")) {


            acct.setBalance(acct.getBalance()


               - (trade.getShares() * trade.getPrice()));


         } else {


            acct.setBalance(acct.getBalance()


               + (trade.getShares() * trade.getPrice()));


         }





         //insert the trade and update the account


         long tradeId = tradeDao.insertTrade(trade);


         compController.registerAction(compId, "insertTrade", tradeId);







         acctDao.updateAcct(acct);


         compController.registerAction(compId, "updateAcct", oldBalance);





         //close the compensation scope


         compController.stopScope(compId);







      } catch (Exception up) {


         //reverse the individual database operations


         compController.compensate(compId);




         throw up;


      }


   }


}


 

在查看 清单 3 的过程中,要注意定义事务工作单元时,要首先使用 startScope() 方法开始补偿范围。然后,必须保存原始余额,以便在注册活动时将它传递给协调器。活动完成后,使用 registerAction() 方法,注册该活动。这会告诉补偿协调器数据库更新操作已经成功完成并且需要被添加到可能的补偿活动清单。如果整个 LUW 成功结束,则调用 stopScope() 方法,移除补偿协调器中的所有引用。但是,如果发生异常,则调用 compensate() 方法,它会处理所有已经提交到数据库的活动的反转操作。

清单 2 3 中的源代码距离用于生产的标准还比较远,但是它的确阐释了构建您自己的补偿框架涉及的技术。自定义补偿框架可以使用自定义注释、方面(aspect)(或拦截器),甚至是您自己的自定义补偿特定于域的语言(DSL;请参见 参考资料),使代码更加直观。另外您不需要对补偿框架使用 JMS 异步消息传递,但是我发现在处理补偿故障相关的问题时它很有用。

 

 

参考 www.ibm.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值