内容


事务策略

高性能策略

了解如何为高性能应用程序实现事务策略

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 事务策略

敬请期待该系列的后续内容。

此内容是该系列的一部分:事务策略

敬请期待该系列的后续内容。

到目前为止,在本 系列 中,您已经了解了如何实现三个事务策略:

  • API 层策略 策略,适用于带有粗粒度 API 层的业务应用程序
  • 客户机组合 策略,适用于带有较细粒度 API 层的业务应用程序
  • 高并发性 策略,适用于具有高度并发的用户负载的应用程序

在这最后一部分中,我将介绍对于具有高性能需求的应用程序非常有用的事务策略。与高并发性策略类似,高性能策略也要考虑折衷的问题。

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

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

从数据持久化的角度看,进行数据库更新操作的最快速的方式是将本地事务 与数据库存储过程一起使用。本地事务(有时称为数据库事务)是由数据库而不是容器环境管理的事务。不需要在应用程序中编写任何事务逻辑(比如 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) 框架。

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

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

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

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

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

这种技术通常称为放松 ACID(Relaxed ACID)。这是一个典型的事务解决方案,适用于长期运行的事务,这些事务在面向服务的架构中将业务流程执行语言(Business Process Execution 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%)。执行事务补偿是一项开销很大且耗时的操作。如果经常反转数据库更新操作,系统速度会减慢而且与使用其他事务策略相比,这个策略可能会导致整体性能下降。要进行的补偿性更新越多,数据库不一致的风险越大。确保在选择该事务策略之前分析错误率。

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

现有补偿框架

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 概念视图
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() 方法,它会处理所有已经提交到数据库的活动的反转操作。

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

结束语

是否选择使用高性能策略取决于对其利弊的权衡。这个事务策略伴随着很多风险,并且实现起来也很复杂。但是,如果性能是最重要的,并且 您的应用程序很健壮、没有错误,那么这个策略就比较合适,因为它至少了确保了某种程度的数据库完整性和一致性,同时不会严重影响性能。如果在性能不是首要考虑因素的情况下,我还会推荐这类解决方案吗?当然不会。应该一直尝试在应用程序中使用传统的 ACID 属性。但是,如果要牺牲一定程度的数据库一致性和数据完整性来换取性能,那么应该考虑高性能策略。

在本 系列 中,我为您介绍了事务处理相关的缺陷和挑战并介绍了四个事务策略,您可以使用它们为您的应用程序构建健壮的事务解决方案。表面看起来事务处理似乎很简单,但是当您开始将其应用于各种业务应用场景时,它就变得相当复杂了。本系列文章旨在减少这种复杂性,揭示一些技巧和粗略,简化一些看似颇具挑战性的任务 — 维持高度的数据完整性和一致性。我希望这一系列介绍事务的文章为您提供了所需知识,帮助您从事务处理的角度改进应用程序和数据的健壮性。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=419158
ArticleTitle=事务策略: 高性能策略
publish-date=08102009