权衡 Apache Geronimo EJB 事务选项,第 3 部分: 综合所有事务

EJB 事务:我的选择是什么?

在实现 EJB 事务时,有两种选择:容器管理或 bean 管理的事务。

使用容器管理的事务时,将在部署描述符中指定事务行为。EJB 容器负责控制事务边界。您为整个 enterprise bean、bean 上的个别方法或两者指定事务属性。事务属性的选择有:

  • Required
  • RequiresNew
  • Supports
  • Mandatory
  • NotSupported
  • Never

使用 bean 管理的事务时,您编程控制事务边界并决定事务开始、提交和回滚的时机。在 bean 管理的事务中,可以在 Java Transaction API(JTA)或 Java Database Connectivity(JDBC)事务实现之间进行选择。JTA 使用 javax.transaction.UserTransaction 接口控制事务,而 JDBC 事务则直接经 java.sql.Connection 接口执行操作来控制事务行为。

如果使用会话或消息驱动 bean(MDB),那么可以实现 bean 管理或容器管理事务。然而,实体 bean 只可以使用容器管理事务。

如果不确定您的 bean 使用哪种事务类型,Sun Microsystems 建议对 enterprise bean 使用具有 required 属性的容器管理的事务。

对于开发人员,使用容器管理的事务会比较简单并且需更比较少的工作量。在 bean 方法中不要求事务逻辑。在 enterprise bean 的方法级别上划分事务边界。bean 方法必须运行在事务上下文中或不在其中运行。

如果要求对事务边界进行更严格的控制,那么使用 bean 管理的事务。如果期望在 enterprise bean 中拥有长期过程,那么使用 bean 管理的事务。对于本文的目的,希望事务运行尽可能短的时间。如果使用容器管理事务,划分边界的粒度不够细,它们处于 bean 方法级别。

通过使用 bean 管理的事务,可以限制事务的持续时间为短暂的。可以在事务中隔离数据库操作并允许长期过程在事务作用域之外运行。这将确保不会阻塞访问同一数据的其他任何事务。

并发控制策略

使用 EJB 实现并发控制有两种策略:可以遵循消极(pessimistic)或积极(optimistic)锁定策略。

使用消极锁定 时,在修改数据所需的事务持续时间内获取锁,从而阻塞其他任何人修改同一数据。如果系统中有其他人可能试图修改正在处理的数据,那么这是使用该策略的好机会。该策略提供对数据的可靠访问,但是不适用于较小规模的系统,因为当要求系统规模和更多锁时,性能将会降低。

积极锁定 不会在事务期间持有锁。您采取积极的态度并假设在您使用数据时数据不会被其他事务修改。当发生数据冲突时,您可以处理异常情况,但假设这是很少发生的。当 要求更新数据时,实现策略来检查数据在最近被读取之后和在被修改之前这段时间里没有发生改变。如果数据没有改变,就执行更新。该策略适用于大规模系统。缺 点是您必须实现检测和处理数据冲突的代码。

隔离

事务的 ACID 属性之一是隔离性。隔离允许事务行为(无论是读还是写数据)独立或隔离于其他并发运行的事务。通过控制隔离,每个事务在其行动时间里都像是修改数据库的惟一事务。一个事务与其他事务隔离的程度称为隔离级别

锁机制和同步用来控制隔离级别。随着隔离级别增加,需要更多的锁和同步。由于锁控制数据资源,其他尝试执行任何数据操作的事务必须等待,直到锁被释放。因此,增加隔离级别将以性能为代价。相反,随者隔离级别降低,因事务耗费较少的时间来等待锁被释放,将提高性能。

数据一致性

对事务设置隔离级别来处理下列数据一致性问题:

  • 脏数据读(Dirty read)
  • 不可重复读(Unrepeatable read)
  • 影像读(Phantom read)

脏数据读

当从数据库读取未经提交的数据时,发生脏数据读。读取的数据没有与数据库里的真实数据同步。

考虑下面的场景,其中两个事务在读取和更新数据库的 String 字段 X。String X 的初始值是 foo

  1. 事务 1 读取 String X 的值 foo
  2. 事务 1 将 String X 的当前值与 bar 连接并将其保存到数据库中。
  3. X 的新值是 foobar。事务 1 还没有发出提交语句。
  4. 事务 2 读取 String X 的值,即 foobar
  5. 事务 1 中止。
  6. 事务 2 将 String X 与 bar 连接并将其保存至数据库。
  7. X 的新值是 foobarbar,这时其正确的值应该是 foobar
  8. 事务 2 对 String X 的值执行了脏数据读。

这里的问题是一个事务可以改变值,而另一个事务可能在初始修改被提交前读取其值。数据是脏的,并不能表示数据的真实状态。

不可重复读

如果应用程序从数据库读取数据,然后又重读已经被修改的数据(可能以后在同一事务中),那么会发生不可重复读。考虑下面的场景,其中两个应用程序在读取和更新同一数据:

  1. 应用程序 1 读取 String X 的值 foo
  2. 应用程序 2 更新 String X 的值为 foobar
  3. 应用程序 1 重读 String 的值并发现其已改为 foobar

因此在读取之间,数据的值已改变并变得不一致。

影像读

影像读 与不可重复读类似。但是,影像读会将新数据插入到数据库中。应用程序从数据库中读取数据集,然后确定在其重读同一数据集时,附加的数据已经被添加。考虑下面的场景,其中两个应用程序在读取和更新数据库的同一数据:

  1. 应用程序 1 搜索符合某个条件的数据并返回五行的数据集。
  2. 应用程序 2 将五个满足应用程序 1 的搜索条件的附加行添加到数据库。
  3. 在应用程序 1 基于其初始条件重读数据库(且期望得到五行)时,将返回 10 行。

同样,在读取之间数据变得不一致。

选择隔离级别

隔离的四个级别列在下面,按最低(最弱)隔离级别到最高(最强)级别的顺序排列。记住当提高隔离级别时,应用程序的性能将降低。

  • Read uncommitted - 该选项仅适用于具有非共享数据的非任务关键型系统(这在应用程序中是很少见的情况)。性能处于最佳状态,但是将牺牲并发控制。如果确定没有其他并发事务,请使用该选项。使用该选项,以上所列的数据问题都无法解决。
  • Read committed - 这是大多数数据库的默认隔离级别,也是 Apache Geronimo 默认的。只能读取提交的数据,因此该选项解决了脏数据读问题。由于要求对数据库使用附加锁,因此性能将会慢一些。
  • Repeatable read - 通过使用该隔离级别,解决脏数据读和未提交读问题。可以保证读取的任何行可以在以后被重读而其值将不会改变。
  • Serializable - 这是最严格的隔离级别,解决了所有三个数据问题。在希望事务以真正隔离的方式运行并完全与其他事务独立时,请使用该级别。这将保证数据一致性。对任务关键型系统使用它来保证真正地隔离事务行为。但是注意,该隔离级别是以性能为代价。

 

在 bean 管理的事务中的隔离级别

通过底层的数据库资源管理器指定隔离级别。使用 bean 管理的事务时,可以编程访问底层连接。由于可以访问 java.sql.Connection 接口,可以使用方法 setTransactionIsolation(int level) 改变连接的隔离级别。

使用下面这些常量来设置恰当的隔离级别:

  • Connection.TRANSACTION_READ_UNCOMMITTED
  • Connection.TRANSACTION_READ_COMMITTED
  • Connection.TRANSACTION_REPEATABLE_READ
  • Connection.TRANSACTION_SERIALIZABLE

其他让人感兴趣的方法是:

  • Connection.getTransactionIsolation()
  • DatabaseMetaData.supportsTransactionIsolationLevel(int)

    (参考 参考资料 部分中的 Sun JavaDoc API 来获得有关这些方法的更多信息。)

    注意:通过修改隔离级别,要求数据库资源管理器改变该资源的隔离性。这不是数据库供应商必须提供的必要特性。实际上,很多数据库供应商不会允许这么做。在改变隔离级别时一定要小心。查看您的数据库资源管理器文档来查明支持的隔离级别。

    另外,应该在开始事务之前设置事务隔离级别。决不能在事务的过程中切换隔离级别。大多数资源管理器还要求对事务中的所有参与者使用同一隔离级别。

    在容器管理的事务中的隔离级别

    在使用容器事务时,没有办法在部署描述符中指定隔离级别。默认情况下,Geronimo 对 EJB 容器使用 Read Committed 隔
    离级别。如果需要对隔离级别更细的控制,那么考虑在 bean 管理的事务中使用 JDBC 事务。

事务超时

在 bean 管理的事务中使用 JTA 事务时,可以使用 javax.transaction.UserTransaction 接口的 setTransactionTimeout 方法。这将设置在事务中止之前将运行的最大时间(秒)。

分布式事务

在单个事务中的多个参与者物理分布于网络中时,事务称为分布式事务。分布式事务允许不同类型的资源参与到事务中。分布式事务的例子是:

  • 单个会话 bean 开始事务并更新数据库 A。调用运行在同一个应用服务器上的另一个会话 bean 来更新数据库 B。第一个会话 bean 提交事务。两个数据库更新发生在同一个事务中。
  • 单个会话 bean 开始事务并更新数据库 A。调用运行在不同的 应用服务器上的另一个会话 bean 来更新数据库 B。每个应用服务器的事务管理器将确保两个数据库在同一事务中更新。
  • 单个会话 bean 开始事务并更新数据库 A,接着是 Java Message Service(JMS)操作。两个工作单元都是同一事务的一部分。如果 JMS 操作失败,事务将不会更新数据库。

几个事务管理器必须共同工作来执行分布式事务。通常指定一个单独的事务管理器(称为事务协调器分布式事务管理器)来协调其他事务管理器。

事务管理器进而与资源管理器进行协调来对资源(可能是数据库或消息服务器)执行必需的提交或回滚。大多数数据库的事务管理器和资源管理器紧密地耦合在一起。

posted @ 2008-03-20 09:42  谢芳[Kevin]  阅读(282)  评论(0编辑  收藏  举报