Java理论和实践:理解 JTS?平衡安全性和性能

1/5/2008来源:Java教程人气:4589


  级别:中级

Brian Goetz(brian@quiotix.com)
首席顾问,Quiotix Corp
2002 年 5 月

在他的关于 JTS 的系列文章的第 1 和第 2 部分,Brian 讲述了一些基础知识,包括什么是事务以及 J2EE 容器如何使事务服务对 EJB 组件透明。尽管能够以声明的方式而不是编程的方式指定组件的事务性语义可以大大增强配置企业应用程序时的灵活性,但在装配应用程序时做出不当的决定会削弱应用程序的性能和稳定性。在这最后一部分,Brian 讨论了 J2EE 提供的用来治理事务划分和隔离的工具和一些高效率地使用这些工具的指导。请单击文章顶部或底部的讨论,在讨论论坛与作者和其他读者分享您对本文的想法。
在本系列的第 1 部分(“An introdUCtion to transactions”)和第 2 部分(“The magic beind the scenes”)中,我们定义了什么是事务,列举了事务的基本特性(PRoperty),并探讨了 java 事务服务(Java Transaction Service)和 J2EE 容器如何合作为事务提供对 J2EE 组件的透明支持。在本文中,我们将讨论事务的划分和隔离这个主题。

为 EJB 组件定义事务划分和隔离属性(attribute)的职责由应用程序装配人员来承担。假如这些属性设置不当,会对应用程序的性能、可伸缩性或容错能力造成严重的后果。不幸的是,并没有一种必须遵守的规则用于正确设置这些属性,但有一些指导可以帮助我们在并发危险和性能危险之间找到一种平衡。

我们在第 1 部分中讨论过,事务主要是一种异常处理机制。事务在程序中的用途与合法合同在日常业务中的用途相似:假如出了什么问题它们可以帮助恢复。但由于大多数时间内都没实际发生什么错误,我们就希望能够尽量减少它们的开销以及对其余时间的占用。我们在应用程序中如何使用事务会对应用程序的性能和可伸缩性产生很大的影响。

事务划分
J2EE 容器提供了两种机制用来定义事务的起点和终点:bean 治理的事务和容器治理的事务。在 bean 治理的事务中,用 UserTransaction.begin() 和 UserTransaction.commit() 在 bean 方法中显式开始和结束一个事务。另一方面,容器治理的事务提供了更多的灵活性。通过在装配描述符中为每个 EJB 方法定义事务性属性,您可以指定每个方法的事务性需求并让容器确定何时开始和结束一个事务。无论在哪种情况下,构建事务的基本指导方针都是一样的。

进来,出去
事务划分的第一条规则是“尽量短小”。事务提供并发控制;这通常意味着资源治理器将代表您获得您在事务期间访问的数据项的锁,并且它必须一直持有这些锁,直到事务结束。(请回忆一下本系列第 1 部分所讨论的 ACID 特性,其中“ACID”的“I”代表“隔离”(Isolation)。也就是说,一个事务的结果影响不到与该事务并发执行的其它事务。)当您拥有锁时,任何需要访问您锁定的数据项的其它事务将不得不一直等待,直到您释放锁。假如您的事务很长,那些其它的所有事务都将被锁定,您的应用程序吞吐量将大幅度下降。


规则 1:使事务尽可能短小。

通过使事务尽量短小,您可以把阻碍其它事务的时间缩到最短,从而提高应用程序的可伸缩性。保持事务尽可能短小的最好方法当然是不在事务中间做任何不必要耗费时间的事,非凡是不要在事务中间等待用户输入。

开始一个事务,从数据库检索一些数据,显示数据,然后在仍处于事务中时请用户做出一个选择可能比较诱人。千万别这么做!即使用户注重力集中,也要花费数秒来响应 ? 而在数据库中拥有锁数秒的时间已经是很长的了。假如用户决定离开计算机,或许是去吃午餐或者甚至回家一天,会发生什么情况?应用程序将只好无奈停机。在事务期间执行 I/O 是导致灾难的秘诀。


规则 2:在事务期间不要等待用户输入。

将相关的操作归在一起
由于每个事务都有不小的开销,您可能认为最好是在单个事务中执行尽可能多的操作以使每个操作的开销达到最小。但规则 1 告诉我们长事务对可伸缩性不利。那么如何实现最小化每个操作的开销和可伸缩性之间的平衡呢?

我们把规则 1 设置为逻辑上的极端 ? 每个事务一个操作 ? 这样不仅会导致额外开销,还会危及应用程序状态的一致性。假定事务性资源治理器维护应用程序状态的一致性(请回忆一下第 1 部分,其中“ACID”的“C”代表“一致性”(Consistency)),但它们依靠应用程序来定义一致性的意思。实际上,我们在描述事务时使用的一致性的定义有点圆滑:应用程序说一致性是什么意思它就是什么意思。应用程序把几组应用程序状态的变化组织到几个事务中,结果应用程序的状态就成了定义上的(by definition)一致。然后资源治理器确保假如它必须从故障恢复的话,就把应用程序状态恢复到最近的一致状态。

在第 1 部分中,我们给出了一个在银行应用程序中将资金从一个帐户转移到另一个帐户的示例。清单 1 展示了这个示例可能的 SQL 实现,它包含 5 个 SQL 操作(一个选择,两个更新和两个插入操作):

清单 1. 资金转移的样本 SQL 代码
SELECT accountBalance INTO aBalance
FROM Accounts WHERE accountId=aId;
IF (aBalance >= transferAmount) THEN
UPDATE Accounts
SET accountBalance = accountBalance - transferAmount
WHERE accountId = aId;
UPDATE Accounts
SET accountBalance = accountBalance + transferAmount
WHERE accountId = bId;
INSERT INTO AccountJournal (accountId, amount)
VALUES (aId, -transferAmount);
INSERT INTO AccountJournal (accountId, amount)
VALUES (bId, transferAmount);
ELSE
FAIL "Insufficient funds in account";
END IF




假如我们把这个操作作为五个单独的事务来执行会发生什么情况?这样不仅会使执行速度变慢(由于事务开销),还会失去一致性。例如,假如一个人从帐户 A 取了钱,作为执行第一次 SELECT(检查余额)和随后的记入借方 UPDATE 之间的一个单独事务的一部分,会发生什么情况?这样会违反我们认为这段代码会强制遵守的业务规则 ? 帐户余额应该是非负的。假如在第一次 UPDATE 和第二次 UPDATE 之间系统失败会发生什么情况?现在,当系统恢复时,钱已经离开了帐户 A 但还没有记入帐户 B 的贷方,并且也无记录说明原因。这样,哪个帐户的所有者都不会开心。

清单 1 中的五个 SQL 操作是单个相关操作 ? 将资金从一个帐户转移到另一个帐户 ? 的一部分。因此,我们希望要么全部执行它们,要么一个也不执行,建议在单个事务中全部执行它们。


规则 3:将相关操作归到单个事务中。

理想化的平衡
规则 1 说事务应尽可能短小。清单 1 中的示例表明有时候我们必须把一些操作归到一个事务中来维护一致性。当然,它要依靠应用程序来确定“相关操作”是由什么组成的。我们可以把规则 1 和 3 结合在一起,提供一个描述事务范围的一般指导,我们规定它为规则 4:


规则 4:把相关操作归到单个事务中,但把不相关的操作放到单独的事务中。

容器治理的事务
在使用容器治理的事务时,不是显式声明事务的起点和终点,而是为每个 EJB 方法定义事务性需求。bean 的 assembly-descriptor 的 container-transaction 部分的 trans-attribute 元素中定义了事务模式。(清单 2 中显示了一个 assembly-descriptor 示例。)方法的事务模式以及状态 ? 调用方法是否早已在事务中被征用 ? 决定了当 EJB 方法被调用时容器应该进行下面几个操作中的哪一个:

征用现有事务中的方法。
创建一个新事务,并征用该事务中的方法。
不征用任何事务中的方法。
抛出一个异常。
清单 2. 样本 EJB 装配描述符
<assembly-descriptor>
...
<container-transaction>
<method>
<ejb-name>MyBean</ejb-name>
<method-name>*</method-name>
</method>
<trans-attribute>Required</trans-attribute>
</container-transaction>
<container-transaction>
<method>
<ejb-name>MyBean</ejb-name>
<method-name>logError</method-name>
</method>
<trans-attribute>RequiresNew</trans-attribute>
</container-transaction>
...
</assembly-descriptor>




J2EE 规范定义了六种事务模式:Required、RequiresNew、Mandatory、Supports、NotSupported 和 Never。表 1 概述了每种模式的行为 ? 在现有事务中被调用和不在事务内调用时的行为 ? 并描述了每种模式受哪些类型的 EJB 组件支持。(一些容器可能答应您在选择事务模式时有更多的灵活性,但这种使用要依靠特定于容器的功能,因此不适合跨容器的情况)。

表 1. 事务模式

事务模式 Bean 类型 在事务 T 内被调用时的行为 在事务外被调用时的行为
Required 会话、实体、消息驱动 在 T 中征用 新建事务
RequiresNew 会话、实体 新建事务 新建事务
Supports 会话、消息驱动 在 T 中征用 不带事务运行
Mandatory 会话、实体 在 T 中征用 出错
NotSupported 会话、消息驱动 不带事务运行 不带事务运行
Never 会话、消息驱动 出错 不带事务运行

在只使用容器治理的事务的应用程序中,只有组件调用事务模式为 Required 或 RequiresNew 的 EJB 方法时才启动事务。假如容器创建一个事务作为调用事务性方法的结果,当该方法完成时将关闭该事务。假如方法正常返回,容器将提交事务(除非应用程序已经要求回滚事务)。假如方法通过抛出一个异常退出,容器将回滚事务并传播该异常。假如在现有事务 T 中调用了一个方法,并且事务模式指定应该不带事务运行该方法或者在新事务中运行该方法,那么事务 T 将被暂挂,一直到方法完成,然后先前的事务 T 被恢复。

选择一种事务模式
那么我们应该为自己的 bean 方法选择哪种模式呢?对于会话 bean 和消息驱动 bean,您通常想使用 Required 来确保每个调用都被作为事务的一部分执行,但仍将答应方法作为一个更大的事务的组件。请小心使用 RequiresNew;只有在确定自己的方法的行为应该与调用您的方法的行为分开提交时,才应该使用这种模式。RequiresNew 一般情况下只和与系统中其它对象关系很少或没什么关系的对象(比如日志对象)一起使用。(把 RequiresNew 与日志对象一起使用比较有意义,因为您可能希望在不管外围事务是否提交的情况下提交日志消息。)

RequiresNew 使用不当会导致与上面的描述相似的情况,其中,清单 1 中的代码在五个分开的事务而不是一个事务中执行,这样会使应用程序处于不一致状态。

对于 CMP(容器治理的持久性,container-managed persistence)实体 bean,通常是希望使用 Required。Mandatory 也是一个合理的选项,非凡是在最初开发时;这将会警告您实体 bean 方法在事务外被调用这种情况,这时可能会指出一个部署错误。您几乎从不希望把 RequiresNew 和 CMP 实体 bean 一起使用。NotSupported 和 Never 旨在用于非事务性资源,比如 Java 事务 API(Java Transaction API,JTA)事务中无法征用的外部非事务性系统或事务性系统的适配器。

假如 EJB 应用程序设计得当,应用上面的事务模式指导往往会自然地产生规则 4 建议的事务划分。原因是 J2EE 体系架构鼓励把应用程序分解为最小的方便处理的块,并且每个块都作为一个单独的请求被处理( 不管是以 HTTP 请求的形式还是作为在 JMS 队列中排队的消息的结果)。

重温隔离
在第 1 部分中,我们定义了隔离(isolation)的意思是:一个事务的影响对与该事务并发执行的其它事务是不可见的;从事务的角度来看,好象事务是连续执行而非并行执行。尽管事务性资源治理器经常可以同时处理许多事务并提供隔离的假象,但有时隔离限制实际上要求把新事务延迟到现有事务完成后才开始。由于完成一个事务至少包括一个同步磁盘 I/O(写到事务日志),这就会把每秒的事务数限制到接近每秒的写磁盘次数,这对可伸缩性不利。

实际上,通常是充分放松隔离需求以答应更多的事务并发执行并使系统响应能够得到改善,使可伸缩性变得更强。几乎所有的数据库都支持标准隔离级别:读未提交的(Read Uncommitted)、读已提交的(Read Committed)、可重复的读(Repeatable Read) 和可串行化的(Serializable)。

不幸的是,为容器治理的事务治理隔离目前是在 J2EE 规范的范围之外。但是,许多 J2EE 容器,比如 IBM WebSphere 和 BEA WebLogic,将提供特定于容器的扩展,这些扩展答应您以每方法(per-method)为基础设置事务隔离级别,设置方法与在装配描述符中设置事务模式的方法相同。对于 bean 治理的事务,您可以通过 JDBC 或者其它资源治理器连接设置隔离级别。

为阐明隔离级别之间的差异,我们首先把几个并发危险分类 ? 这几种危险是当没有适当地隔离时一个事务可能会干涉另一个事务的情况。下列的所有这些危险都与这种情况( 第二个事务已经启动后第一个事务变得对第二个事务可见)的结果有关:

脏读(Dirty Read):当一个事务的中间(未提交的)结果对另一个事务可见时就会发生这种情况。


不可重复的读(Unrepeatable Read):当一个事务读取一个数据项,然后重新读取这个数据项并看到不同的值时就是发生了这种情况。


虚读(Phantom Read):当一个事务执行返回多个行的查询,稍后再次执行同一个查询并看到第一次执行该查询没出现的额外行时就是发生了这种情况。
四个标准隔离级别与这三个隔离危险相关,如表 2 所示。最低的隔离级别“读未提交的”并不能保护事务不被其它事务更改,但它的速度最快,因为它不需要争夺读锁。最高的隔离级别“可串行化的”与上面给出的隔离的定义相当;每个事务好象都与其它事务的影响完全隔离。

表 2. 事务隔离级别

隔离级别 脏读 不可重复的读 虚读
读未提交的 是 是 是
读已提交的 否 是 是
可重复的读 否 否 是
可串行化的 否 否 否

对于大多数数据库,缺省的隔离级别为“读已提交的”,这是个很好的缺省选择,因为它阻止事务在事务中的任何给定的点看到应用程序数据的不一致视图。“读已提交的”是一个很不错的隔离级别,用于大多数典型的短事务,比如获取报表数据或获取要显示给用户的数据的时候(多半是作为 Web 请求的结果),也用于将新数据插入到数据库的情况。

当您需要所有事务间有较高级别的一致性时,使用较高的隔离级别“可重复的读”和“可串行化的”比较合适,比如在清单 1 示例中,您希望从检查余额以确保有足够的资金到您实际取钱期间账户余额一直保持不变;这就要求至少要用“可重复的读”隔离级别。在数据一致性绝对重要的情况下,比如审核记帐数据库以确保一个帐户的所有借方金额和贷方金额的总数等于它目前的余额时,可能还需要防止创建新行。这种情况下就需要使用“可串行化的”隔离级别。

最低的隔离级别“读未提交的”很少使用。它适用于您只需要获得近似值,否则查询将导致您不希望的性能开销这种情况。当您想要估计一个变化很快的数量,如定单数或者今天所下定单的总金额(以美元为单位)时一般使用““读未提交的”。

因为隔离和可伸缩性之间实际是一种此消彼长的关系,所以您在为事务选择隔离级别时应该小心行事。选择太低的级别对数据比较危险。选择太高的级别可能对性能不利,尽管负载比较轻时可能不会这样。一般来说,数据一致性问题比性能问题更严重。假如拿不准,应该以小心为主,选择一个较高的隔离级别。这就引出了规则 5:


规则 5:使用保证数据安全的最低隔离级别,但假如拿不准,请使用“可串行化的”。

即使您打算刚开始时以小心为主并希望结果性能可以接受 ?(被称为“拒绝和祈祷(denial and prayer)”的性能治理技术 ? 很可能是最常用的性能策略,尽管大多数开发者都不承认这一点),在开发组件时考虑隔离需求也是有利的。您应该努力编写能够容忍级别较低但实用的隔离级别的事务,这样,当稍后性能成为问题时,自己就不会陷入困境。因为您需要知道方法正在做什么以及这个方法中隐藏了什么一致性假设来正确设置隔离级别,那么在开发期间仔细说明并发需求和假设,以便在装配应用程序时帮助作出正确的决定也不失为一个好主意。

结束语
本文中提供的许多指导可能看起来有点互相矛盾,因为象事务划分和隔离这种问题本来就是此消彼长的。我们正在努力平衡安全性(假如我们不关心安全性,那就压根不必用事务了)和我们用来提供安全限度的工具的性能开销。正确的平衡要依靠许多因素,包括与系统故障或当机时间相关的代价或损害以及组织的风险承受能力。

参考资料

请单击文章顶部或底部的讨论参与本文的讨论论坛。


JTS 系列的第 1 部分(“An introduction to transactions”)讲述了一些基础知识,包括什么是事务,以及事务对于构建可靠的分布式应用程序来说至关重要的原因。第 2 部分(“The magic beind the scenes”)探讨了 Java 事务服务(Java Transaction Service)和 J2EE 如何合作为事务提供对 J2EE 组件的透明支持。


阅读 Brian Goetz 的所有 Java 理论和实践专栏。


Jim Grey 和 Andreas Reuter 合著的 Transaction Processing: Concepts and Techniques 是关于事务处理这个主题的权威著作。


Philip Bernstein 和 Eric Newcomer 合著的 Principles of Transaction Processing 是关于这个主题的很好的介绍;它包含了许多历史和概念。


Ed Roman、Tyler Jewell 和 Scott Ambler 合著的 Mastering Enterprise JavaBeans 是关于 J2EE 和 EJB 技术的很好的介绍。


通过 Transaction Management under J2EE 1.2(JavaWorld,2000 年 7 月)学习声明式事务划分的基础知识。


来自 WebSphere 开发者园地的这篇技术记要量化减少隔离级别的好处。


白皮书“WebSphere application Server Development Best Practices for Performance and Scalability”描述了用来治理 J2EE 应用程序性能和可伸缩性的几种很有用的技术。


Visual Age 开发者园地上发表了一篇文章,这篇文章检查了 J2EE 应用程序中的数据库访问和并发治理问题。


PreciseJava 中的教程“Best practices to improve performance in JDBC”描述了几种有用的数据库应用程序性能最优化。


请在 developerWorks Java 技术专区中查找其它关于 Java 技术的内容。

关于作者
Brian Goetz 是一位软件顾问,在过去 15 年间一直从事专业软件开发。他是 Quiotix,一家位于加利福尼亚,洛斯拉图斯(Los Altos)的软件开发和咨询公司的总顾问。请参阅流行的业界出版物中 Brian 已经发表和即将发表的文章。您可以通过 brian@quiotix.com 与 Brian 联系。

--摘自IBM developerWorks Java网站技术专区 .