分类: Java
2009-07-19 20:39:41
混淆事务模型与事务策略是一个常见的错误。本系列关于 事务策略 的第二篇文章将概述 Java™ 平台支持的三种事务模型,并介绍使用这些模型的四种主要事务策略。通过使用 Spring Framework 和 Enterprise JavaBeans (EJB) 3.0 规范中的示例,Mark Richards 将解释事务模型的运行原理以及它们如何形成开发各种事务策略(从基本的事务处理到高速事务处理系统)的基础。
开发人员、设计人员和架构师经常会混淆事务模型 与事务策略。我经常会让与客户接触的架构师和技术总监描述他们项目的事务策略。我通常会获得三种回应。有时,他们会说 “我们实际上并未在应用程序中使用事务。”另一些时候,我会听到迷惑的回答:“我不明白你的意思。”但是,我也会遇到非常自信的回答:“我们使用声明式事务。”在本文中,术语声明式事务 描述的是一个事务模型,但它绝不是一种事务策略。
|
Java 平台支持的三种事务模型包括:
这些模型描述事务在 Java 平台中的基本运行方式,以及它们是如何实现的。但是,它们仅提供了事务处理的规则和语义。如何应用事务模型则完全由您决定。举例来说,应该如何在 REQUIRED
和 MANDATORY
事务属性之间做出选择?您应该在何时何种情况下指定事务回滚指令?您应该在何时考虑 Programmatic Transaction 模型与 Declarative Transaction 模型的优劣?您应该如何优化高性能系统的事务?事务模型本身无法回答这些问题。您必须通过开发自己的事务策略或采用本文介绍的四种主要事务策略之一来解决它们。
如本系列的 第一篇文章 所述,许多常见的事务陷阱都会影响到事务行为,并且由此会降低数据的完整性和一致性。同样,缺乏有效的(或任何)事务策略将对您数据的完整性和一致性造成负面影响。本文所描述的事务模型是开发有效事务策略的基本元素。理解这些模型之间的差异以及它们的运行方式对于理解使用它们的事务策略非常重要。在介绍完三种事务模型之后,我将讨论适用于大多数业务应用程序(从简单的 Web 应用程序到大型的高速事务处理系统)的四种事务策略。 事务策略 系列的后续文章将详细讨论这些策略。
在 Local Transaction 模型中,事务由底层数据库资源管理程序(而非应用程序所在的容器或框架)管理,这便是它得名的原因。在这个模型中,您将管理连接,而不是事务。从 “了解事务陷阱” 一文可知,在使用对象关系映射框架,如 Hibernate、TopLink 或 the Java Persistence API (JPA),执行数据库更新时,您不能使用 Local Transaction 模型。在使用数据访问对象(DAO)或基于 JDBC 的框架和数据库存储过程时,您可以使用它。
您可以采用以下两种方式来使用 Local Transaction 模型:让数据库来管理连接,或者以编程的方式管理连接。要让数据库管理连接,您需要将 JDBC Connection
对象上的 autoCommit
属性设置为 true
(默认值),这将通知底层数据库管理系统(DBMS)在完成插入、更新或删除操作之后提交事务,或者在操作失败时返回任务。清单 1 展示了这种技巧,它在 TRADE
表中插入了一条股票交易命令:
public class TradingServiceImpl { public void processTrade(TradeData trade) throws Exception { Connection dbConnection = null; try { DataSource ds = (DataSource) (new InitialContext()).lookup("jdbc/MasterDS"); dbConnection = ds.getConnection(); dbConnection.setAutoCommit(true); Statement sql = dbConnection.createStatement(); String stmt = "insert into TRADE ..."; sql.executeUpdate(stmt1); } finally { if (dbConnection != null) dbConnection.close(); } } } |
注意,在清单 1 中,autoCommit
值设置为 true
,这将指示 DBMS 应该在各数据库语句之后提交本地事务。如果在逻辑工作单元(LUW)中维护一个数据库活动,那么这一技巧将非常有用。但是,假设清单 1 中的 processTrade()
方法还将更新 ACCT
表中的余额以反映交易订单的值。在本例中,两个数据库操作是相互独立的,针对 TRADE
表的插入操作将在更新 ACCT
表之后提交给数据库。若 ACCT
表更新失败,没有任何机制可以回滚到对 TRADE
表的更新操作,从而造成数据库中的数据不一致。
此场景又引出了第二个技巧:以编程的方式管理连接。在此技巧中,您将 Connection
对象上的 autoCommit
属性设置为 false
,并手动提交或回滚连接。清单 2 演示了此技巧:
public class TradingServiceImpl { public void processTrade(TradeData trade) throws Exception { Connection dbConnection = null; try { DataSource ds = (DataSource) (new InitialContext()).lookup("jdbc/MasterDS"); dbConnection = ds.getConnection(); dbConnection.setAutoCommit(false); Statement sql = dbConnection.createStatement(); String stmt1 = "insert into TRADE ..."; sql.executeUpdate(stmt1); String stmt2 = "update ACCT set balance..."; sql.executeUpdate(stmt2); dbConnection.commit(); } catch (Exception up) { dbConnection.rollback(); throw up; } finally { if (dbConnection != null) dbConnection.close(); } } } |
注意,在清单 2 中,autoCommit
属性设置为 false
,这将通知底层 DBMS 在代码而非数据库中管理连接。在本例中,如果一切正常,您必须调用 Connection
对象上的 commit()
方法;否则,如果出现异常,则调用 rollback()
方法。通过这种方式,您可以在相同的工作单元中协调两个数据库活动。
虽然 Local Transaction 模型现在看来有些过时,但它是本文结束部分介绍的一个主要事务策略的重要元素。
Programmatic Transaction 模型的名称来自这样一个事实:开发人负责管理事务。在 Programmatic Transaction 模型中,与 Local Transaction 模型不同,您将管理事务,并且将与底层数据库连接 相分离。
与 清单 2 中的示例相似,借助此模型,开发人员将负责从事务管理程序获取一个事务,启动该事务,提交事务,以及(如果出现异常)回滚到事务。您可能已经猜到,这会产生大量易于出错的代码,从而对应用程序中的业务逻辑造成影响。但是,一些事务策略需要使用 Programmatic Transaction 模型。
虽然概念是相同的是,但 Programmatic Transaction 模型的实现在 Spring Framework 和 EJB 3.0 规范中是不同的。我将先使用 EJB 3.0 来演示这个模型的实现,然后使用 Spring Framework 来展示相同的数据库更新。
在 EJB 3.0 中,您将从事务管理程序(换句话说,容器)获取一个事务,方法是对 javax.transaction.UserTransaction
执行 Java Naming and Directory Interface (JNDI) 查找。获取 UserTransaction
之后,您可以调用 begin()
方法来启动事务,调用 commit()
方法来提交事务,并且调用 rollback()
方法以便在出现错误时回滚事务。在此模型中,容器不会自动提交或回滚事务;开发人员需要自己在执行数据库更新的方法编写此行为。清单 3 通过 JPA 展示了使用 EJB 3.0 的 Programmatic Transaction 模型:
@Stateless @TransactionManagement(TransactionManagementType.BEAN) public class TradingServiceImpl implements TradingService { @PersistenceContext(unitName="trading") EntityManager em; public void processTrade(TradeData trade) throws Exception { InitialContext ctx = new InitialContext(); UserTransaction txn = (UserTransaction)ctx.lookup("UserTransaction"); try { txn.begin(); em.persist(trade); AcctData acct = em.find(AcctData.class, trade.getAcctId()); double tradeValue = trade.getPrice() * trade.getShares(); double currentBalance = acct.getBalance(); if (trade.getAction().equals("BUY")) { acct.setBalance(currentBalance - tradeValue); } else { acct.setBalance(currentBalance + tradeValue); } txn.commit(); } catch (Exception up) { txn.rollback(); throw up; } } } |
在带无状态会话 bean 的 Java EE 容器环境中使用 Programmatic Transaction 模型时,您必须通知容器您正在使用编程事务。为此,您需要使用 @TransactionManagement
注释并将事务类型设置为 BEAN
。如果您未使用此注释,则容器将假定您使用声明式事务管理(CONTAINER
),它是 EJB 3.0 的默认事务类型。在无状态会话 bean 上下文外部的客户机层中使用编程事务时,您不需要设置事务类型。
Spring Framework 提供了两种实现 Programmatic Transaction 模型的方法。一种是通过 Spring TransactionTemplate
来实现,另一种是直接使用 Spring 平台事务管理程序。由于我并不热衷于编写匿名内部类和难以理解的代码,因此我将使用第二种技巧来演示 Spring 中的 Programmatic Transaction 模型。
Spring 至少有九种平台事务管理程序。最常用的包括 DataSourceTransactionManager
、HibernateTransactionManager
、JpaTransactionManager
和 JtaTransactionManager
。我的代码示例使用的是 JPA,因此我将展示 JpaTransactionManager
的配置。
要在 Spring 中配置 JpaTransactionManager
,只需要使用 org.springframework.orm.jpa.JpaTransactionManager
类在应用程序上下文 XML 文件中定义 bean,并添加一个到 JPA Entity Manager Factory bean 的引用。然后,假定包含应用程序逻辑的类是由 Spring 管理的,将事务管理程序注入到 bean 中,如清单 4 所示:
如果 Spring 未管理应用程序类,那么您可以在您的方法中引用事务管理程序,方法是在 Spring 上下文中使用 getBean()
方法。
在源代码中,您现在可以使用平台管理程序获取一个事务。执行了所有更新之后,您可以调用 commit()
方法来提交事务,或者调用 rollback()
方法来回滚事务。清单 5 展示了此技巧:
public class TradingServiceImpl { @PersistenceContext(unitName="trading") EntityManager em; JpaTransactionManager txnManager = null; public void setTxnManager(JpaTransactionManager mgr) { txnManager = mgr; } public void processTrade(TradeData trade) throws Exception { TransactionStatus status = txnManager.getTransaction(new DefaultTransactionDefinition()); try { em.persist(trade); AcctData acct = em.find(AcctData.class, trade.getAcctId()); double tradeValue = trade.getPrice() * trade.getShares(); double currentBalance = acct.getBalance(); if (trade.getAction().equals("BUY")) { acct.setBalance(currentBalance - tradeValue); } else { acct.setBalance(currentBalance + tradeValue); } txnManager.commit(status); } catch (Exception up) { txnManager.rollback(status); throw up; } } } |
注意清单 5 中 Spring Framework 与 EJB 3.0 之间的差异。在 Spring 中,获取事务(随后启动它)的方法是在平台事务管理程序上调用 getTransaction()
。匿名 DefaultTransactionDefinition
类包含关于事务及其行为的详细信息,包括事务名称、隔离级别、传播模式(事务属性)和事务超时(如果存在)。在本例中,我可以仅使用默认值,其名称是空字符串,底层 DBMS 的默认的隔离级别通常为 READ_COMMITTED
,事务属性是 PROPAGATION_REQUIRED
,以及 DBMS 的默认超时。还需注意,commit()
和 rollback()
方法是使用平台事务管理程序调用的,而不是事务(与 DJB 的情况相同)。
Declarative Transaction 模型,又称作 Container Managed Transactions (CMT),是 Java 平台中最常用的事务模型。在该模型中,容器环境负责启动、提交和回滚事务。开发人员仅负责指定事务的行为。本系列的 第一篇文章 所讨论的大多数事务陷阱都与 Declarative Transaction 模型相关。
Spring Framework 和 EJB 3.0 都利用注释来指定事务行为。Spring 使用 @Transactional
注释,而 EJB 3.0 使用 @TransactionAttribute
注释。在使用 Declarative Transaction 模型时,容器将不会针对检测到的异常自动回滚事务。开发人员必须指定出现异常时在何处以及何时回滚事务。在 Spring Framework 中,您通过使用 @Transactional
注释上的 rollbackFor
属性来指定它。在 EJB 中,您通过调用 SessionContext
上的 setRollbackOnly()
方法来指定它。
清单 6 展示了 Declarative Transaction 模型的 EJB 应用:
@Stateless public class TradingServiceImpl implements TradingService { @PersistenceContext(unitName="trading") EntityManager em; @Resource SessionContext ctx; @TransactionAttribute(TransactionAttributeType.REQUIRED) public void processTrade(TradeData trade) throws Exception { try { em.persist(trade); AcctData acct = em.find(AcctData.class, trade.getAcctId()); double tradeValue = trade.getPrice() * trade.getShares(); double currentBalance = acct.getBalance(); if (trade.getAction().equals("BUY")) { acct.setBalance(currentBalance - tradeValue); } else { acct.setBalance(currentBalance + tradeValue); } } catch (Exception up) { ctx.setRollbackOnly(); throw up; } } } |
清单 7 演示了 Declarative Transaction 模型的 Spring Framework 应用:
public class TradingServiceImpl { @PersistenceContext(unitName="trading") EntityManager em; @Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class) public void processTrade(TradeData trade) throws Exception { em.persist(trade); AcctData acct = em.find(AcctData.class, trade.getAcctId()); double tradeValue = trade.getPrice() * trade.getShares(); double currentBalance = acct.getBalance(); if (trade.getAction().equals("BUY")) { acct.setBalance(currentBalance - tradeValue); } else { acct.setBalance(currentBalance + tradeValue); } } } |
除了回滚指令之外,您还必须指定事务属性,这将定义事务的行为。Java 平台支持六种事务属性,而与您使用的是 EJB 还是 Spring Framework 无关:
Required
Mandatory
RequiresNew
Supports
NotSupported
Never
在描述这些事务属性时,我将使用一个假想的 methodA()
方法,事务属性将应用于这个方法。
如果为 methodA()
指定了 Required
事务属性,并且在已有事务作用域内调用了 methodA()
,那么将使用已有的事务作用域。否则,methodA()
将启动一个新的事务。如果事务是由 methodA()
启动的,则它也必须由 methodA()
来终止(提交或回滚)。这是最常用的事务属性,并且是 EJB 3.0 和 Spring 的默认属性。遗憾的是,它的使用并不正确,从而造成了数据完整性和一致性问题。对于本系列后续文章将要讨论的各事务策略,我将更加详细地阐述这个事务属性。
如果为 methodA()
指定了 Mandatory
事务属性,并且在已有事务作用域内调用了 methodA()
,那么将使用已有的事务作用域。但是,如果调用 methodA()
时没有事务上下文,则会抛出一个 TransactionRequiredException
,指示必须在调用 methodA()
之前呈现事务。这个事务属性应用于本文下一部分将要讨论的 Client Orchestration 事务策略。
RequiresNew
事务属性非常有趣。许多时候,我会发现这个属性被误用或误解。methodA()
指定了 RequiresNew
属性,并且在有或没有事务上下文的情况下调用了 methodA()
,那么新事务将始终由 methodA()
启动(和终止)。这意味着,如果在另一个事务(比如说 Transaction1
)的上下文中调用了 methodA()
,那么 Transaction1
将暂停,同时会启动一个新的事务(Transaction2
)。当 methodA()
结束时,Transaction2
将提交或回滚,并且 Transaction1
将恢复。这显然违背了事务的 ACID(atomicity、consistency、isolation 和 durability)属性(特别是 atomicity 属性)。换句话说,所有数据库更新都不再包含在一个单一的工作单元中。如果 Transaction1
将进行回滚,则由 Transaction2
提交的更改将仍然被提交。如果是这种情况,那么事务属性有什么好处呢?如本系列第一篇文章所述,这个事务属性仅用于独立于底层事务的数据库操作(比如审计或记录,在本例中为 Transaction1
)。
Supports
事务属性也是我发现大多数开发人员未完全理解或掌握的一个地方。如果为 methodA()
指定了 Supports
事务属性,并且在已有事务的作用域中调用了 methodA()
,则 methodA()
将在该事务的作用域中执行。但是,如果在没有事务上下文的情况下调用了 methodA()
,则不会启动任何事务。此属性主要用于针对数据库的只读操作。如果是这种情况,为何不指定 NotSupported
事务属性(在下一段中讨论)呢?毕竟,该属性将确保方法在没有事务的情况下运行。答案非常简单。在已有事务的上下文中调用查询操作会导致从数据库事务日志中读取数据(换句话说,已更新的数据),而不在事务作用域中运行会导致查询从表中读取未修改的数据。举例来说,如果您插入一个新的交易订单到 TRADE
表中,然后(在相同的事务中)获取所有交易订单的列表,则未提交的交易将出现在列表中。但是,如果您使用的是 NotSupported
事务属性,那么它会造成数据库查询从表中而不是从事务日志中读取数据。因此,在上一个示例中,您将不会看到未提交的交易。这并不一定是一件坏事;这将由您的用例和业务逻辑决定。
NotSupported
事务属性指定被调用的方法将不使用或启动事务,无论是否呈现了事务。如果为 methodA()
指定了 NotSupported
事务属性,并且在某个事务的上下文中调用了 methodA()
,则该事务将暂停直到 methodA()
结束。当 methodA()
结束时,原始事务将被恢复。这个事务属性只有少许用例,并且它们主要涉及数据库存储过程。如果您尝试在已有事务上下文的作用域中调用数据库存储过程,并且数据库存储过程包含一个 BEGIN TRANS
,或者对于 Sybase 的情况,在未连接模式中运行,则会抛出一个异常,指示已经存在一个事务无法启动新事务。(换句话说,内嵌事务不受支持)。几乎所有的容器都使用 Java Transaction Service (JTS) 作为默认的 JTA 事务实现。不支持内嵌事务的是 JTS 而不是 Java 平台。如果您无法修改数据库存储过程,那么您可以使用 NotSupported
属性来暂停已有事务上下文,以避免这种致命的异常。但是,其影响是您不能在相同的 LUW 中对数据库执行原子更新。这是一种权衡,但有时可以让您迅速脱离困境。
Never
或许是最有趣的事务属性。它的行为类似于 NotSupported
事务属性,但存在一个重要差异:如果使用 Never
事务属性调用方法时已经存在某个事务上下文,则会抛出一个异常,指示您在调用该方法时不允许使用这个事务。我能想到的针对此事务属性的唯一一个用例就是用于测试。它提供了一种简单的方法,用于验证当您调用特定的方法时某个事务是否存在。如果您在调用相应的方法时使用了 Never
事务属性并且接收到了一个异常,则知道事务已经呈现。如果允许执行方法,则您知道事务未被呈现。这是确保事务策略坚固的理想方法。
|
本文描述的事务模型形成了即将介绍的事务策略的基础。在开始构建事务策略之前,完全理解这些模型之间的差异以及它们的运行原理是非常重要的。可以在大多数业务应用程序场景中使用的主要事务策略包括:
我将在此处简要介绍这些策略,并将在本系列的后续文章中详细讨论它们。
当来自客户机层的基于多服务器或基于模型的调用完成单一工作单元时,将使用 Client Orchestration 事务策略。此处的客户机层可以表示来自 Web 框架、门户应用程序、桌面系统或工作流产品或业务流程管理(BPM)组件的调用。从本质上说,客户机层拥有处理流以及完成特定请求所需的 “步骤”。举例来说,要添加一个交易订单,假定您需要将交易插入到数据库中,然后更新客户的帐户余额以反映交易的值。如果应用程序的 API 层非常细粒度化,那么您需要调用客户机层中的两个方法。在此场景中,事务工作单元必须位于客户机层中以确保自动化的工作单元。
当粗粒度方法充当后端功能的主要入口点时,将使用 API Layer 事务策略。(如果愿意,可以将它们称作 services)。在此场景中,客户机(基于 Web、基于 Web 服务、基于消息或者甚至桌面)向后端发起单个调用以执行特定的请求。使用上文提到的交易订单场景,在本例中您将拥有一个入口点方法(比如说 processTrade()
)由客户机层调用。然后,这个方法将包含插入交易订单和更新帐户所需的业务流程。我将这个策略指定为这个名称的原因是,在大多数情况下,后端处理功能会通过使用接口或 API 向客户应用程序公开。这是最常用的事务策略之一。
High Concurrency 事务策略是 API Layer 事务策略的一个变体,它用于不支持通过 API 层长时间运行事务的应用程序(通常出于性能或可伸缩性需求的原因)。顾名思义,此策略主要在从用户的角度支持高度并发性的应用程序中使用。事务在 Java 平台的开销非常大。根据您所使用的数据库,它们可以造成在数据库中出现锁定,独占资源,在吞吐量方面拖慢应用程序,并且在某些情况下甚至造成数据库死锁。这种事务策略内部的主要思想是缩短事务作用域,以便您能够最大限度地减少数据库中的死锁,同时仍然为任何特定的客户机请求维持自动工作单元。在某些情况下,您可能需要重构应用程序逻辑,以支持这种事务策略。
High-Speed Processing 事务策略或许是事务策略的最极端。如果您需要从应用程序中获取最快的处理时间(以及吞吐量),并且仍然在处理中维持一定程序的事务原子性,那么可以使用它。虽然这种策略从数据完整性和一致的角度来说引入了一些风险,但如果正确实现,它将成为 Java 平台中速度可能是最快的事务策略。它可能是这四个事务策略中最难以实现的一个。
从这篇概述中可以看到,开发一个有效的事务策略并非总是一项简单的任务。在解决数据完整性和一致问题时需要考虑各种事项、选项、模型、框架、配置和技巧。在使用应用程序和事务的多年经验中,我发现虽然模型、选项、设置和配置的组合有时会非常伤脑筋,但大多数情况下实际上只有一些选项和设置的组合是有用的。我设计的这四种事务策略将在本系列的后续文章中详细讨论,并且应该能涵盖您可能会在 Java 平台业务应用程序开发中遇到的大多数场景。需要注意:这些策略并不是简单的 “一劳永逸” 解决方案。在某些情况下,一些实现还需要重新构建源代码或者重新设计应用程序。对于这种情况,您只需要问自己一个问题:“我的数据的完整性和一致性有多重要?”在大多数情况下,与损坏数据招致的风险和成本相比,重构工作并不算什么。