JavaEE – JPA(2):EJB中的事务管理

在上一篇文章中介绍了事务的基本概念,包括事务的ACID性质,以及事务的类型并且稍微提到了事务划分。

对于任何一个需要持久化数据的应用而言,事务划分(Transaction Demarcation)的规划都是非常重要的一环。如果规划的不好,轻则影响性能,重则会导致在某些情况下数据发生不一致,从而严重影响业务逻辑的顺利进行。

本文继续介绍事务划分在当前JavaEE技术体系下基于EJB的实现方案。虽然EJB算是JavaEE的亲儿子,但是它的境遇却不怎么好,主要是被EJB 2.x给坑过的开发人员都对他失去了兴趣。加上同时代兴起的spring Framework等轻量级的JavaEE解决方案,就算到了如今EJB 3.x时代,各方面都进步了不少,但还是有不少人拿有色眼睛看待它的。不过,作为JavaEE的正统,还是有必要看看它是如何处理事务划分这一问题的。

事务划分概要

Resource-local事务类型

在上文中我们已经知道Resource-local事务类型实际上就是直接建立在JDBC标准的中的DataSource接口之上的一种事务类型,它直接建立在底层数据库所支持的事务之上,因此十分底层。而在往常的JDBC编程中,这是最常用的一种方式,正因为它的抽象层次低,需要开发人员百分百地控制事务的划分,使用代码指定事务的开始结束等生命周期。随着JavaEE标准的诞生,以及Web开发大行其道,出现的应用服务器(Application Server)则在最底层的基于DataSource接口的事务模型之上抽象出来基于Container的事务类型,这也是当今JavaEE开发中最常见,也是最常用的一种事务类型。下文即将讨论的都属于Container事务的范畴。

Container事务类型

在这种事务类型中,由于应用服务器提供的容器对事务提供了一定程度的支持,因此有下面两种实现思路:

  1. 在容器的帮助下完成自动划分
  2. 使用JTA接口在应用中编码完成显式划分

EJB中的事务划分

回顾了一下事务划分是什么,下面开始正式介绍EJB是如何处理事务划分的。

容器管理的事务(Container-Managed Transaction, CMT)

首先,在EJB中默认的事务划分实现方式就是基于容器管理的事务(CMT)。所以你什么都不指定,那么就会默认使用CMT了。当然也可以显式指定:

@Stateful
@TransactionManagement(TransactionManagementType.CONTAINER)
public class BusinessLogicBean implements IBusinessLogic {
    //  根据不同的元数据定义(通过@TransactionAttribute),其中的方法会被运行在由容器创建的事务中(如果声明了需要事务)
    public void business() {}
}

因此,一个定义其中的方法具体是否需要使用事务,使用的事务具有什么样的特征,都是通过@TransactionAttribute这个注解来完成的。下面就来看看这个注解定义了那些可选项:

  • MANDATORY: 运行当前方法时要求存在处于active状态的事务。如果没有active状态的事务则会抛出异常。
  • REQUIRED: 运行当前方法时,希望处于事务中。当容器碰到这个注解时,会检查当前是否存在active状态的事务。如果存在,就直接使用它;如果不存在,就创建(create)一个供它使用。(这个选项是最常用的,也是默认选项)
  • REQUIRES_NEW: 运行的当前方法如何总是希望能够运行在仅属于自己的事务中,就可以使用这个选项。也就是说,这个方法内对于资源的操作的提交以及回滚都和调用栈中其它事务无关。
  • SUPPORTS: 运行的当前方法对于又没有事务采取一种”无所谓”的态度。有也可以,没有也可以。因此可以推断出在此方法中定义的逻辑通常和资源没有太大的关系。
  • NOT_SUPPORTED: 运行的当前方法对事务有”消极”的态度。如果发现了处于active状态的事务,会尝试让容器挂起(suspend)该事务。因此也可以推断出在此方法中定义的逻辑通常和资源没有太大的关系。
  • NEVER: 当运行的当前方法采用此选项时,如果运行时发现有active状态的事务,那么会直接抛出一个异常。

在上面的各种选项中,出现了事务的创建以及挂起。那么有事务的创建就有事务的提交(回滚),有事务的挂起就有事务的唤起(resume)。这些操作都是由容器完成的,也就是说如果一个方法要求运行在事务中,那么容器会在运行这个的第一行方法前创建一个事务,在完成这个方法的最后一行代码的执行后提交该事务(回滚的话不需要都执行完);如果一个方法要求挂起当前的事务,首先想想为什么会存在这种情况?自问自答,因为一个业务方法可能还会调用另外一个业务方法,而在在这个被调用的内层业务方法中,如果它使用了NOT_SUPPORTED这个选项的话,就会出现需要挂起的要求。回到刚才说的,如果一个方法要求挂起当前的事务,也是由容器来完成这个挂起的操作,然后等该方法运行完毕之后,在回到上一个方法之前,容器会唤起刚才被挂起的事务。

具体的用法可以是这样的:

@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public void businessLogic() {}

如果在执行一个带有事务的方法时没有发生异常,那么在该方法结束的时候,容器就会将该事务提交。但是,如果程序在执行的过程中出现了开发人员可以觉察的异常,比如某些值不符合逻辑,那么就可以通过调用EJBContext对象(这个对象一般通过依赖注入的方式来得到)上的setRollbackOnly()方法来标注这个事务需要被回滚。所谓标注,表示调用这个方法并不会立即导致事务的回滚,而是在将来某个合适的时候再来由容器来执行回滚操作,比如在方法结束的时候,容器会来检查是否需要回滚。如果需要立即中断后续代码的执行,可以采取抛出异常的方式。

Bean管理的事务(Bean-Managed Transaction, BMT)

如果要使用BMT,首先需要在类上定义一些元数据:

@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class BusinessLogicBean implements IBusinessLogic {
    // 此类中的方法会自行定义事务划分
}

BMT最大的特点就是它需要开发人员自行控制事务什么时候开始,什么时候结束。不像CMT那样通常以业务方法的开始和结束作为事务的起点和终点。如果只开始了一个事务,而忘记关闭它,那么会导致异常的发生,同时该事务也会被容器回滚。

而且,BMT类型的事务无法使用从外部进入的事务。比如我们有一个Bean A,它使用的是CMT类型的事务管理方式,其中的一个带有事务的方法调用了采用BMT的Bean B中一个自行管理事务的方法。那么在从A到B这个调用过程中,A中的事务只能有被挂起这一个选择。当B中的方法执行完毕后,该事务会被唤起。

一般而言,选择使用CMT即可。只有在CMT确实解决不了问题的时候才会考虑BMT。因为BMT会增加程序的复杂性,既然应用服务器提供的容器能够帮助你解决事务问题了,为什么还要自己来呢?

那么具体而言,BMT是如何进行事务的相关操作呢?通过javax.transaction.UserTransaction这一接口。

这个接口是JTA(Java Transaction API)中定义的一个接口,它提供了几个方法用于显式地对事务进行操作。在使用了BMT的Bean中可以通过@Resource这一注解将它注入。注入的实际上是一个代理实现,它代表了一个JTA事务对象。

javax.transaction.UserTransaction这个接口并不复杂,定义了以下6个方法:

public interface UserTransaction {
    void begin();
    void commit();
    int getStatus();
    void rollback();
    void setRollbackOnly();
    void setTransactionTimeout(int seconds);
}

注意到这个接口中并没有定义用于挂起/唤起事务的方法,比如suspend()以及resume()。这样做也是有其目的的,因为规范制定者认为这样的操作对于应用逻辑的开发者而言是不需要的。如果提供了只会增加犯错的机会,所以挂起和唤起的操作只能由容器通过其内部的API来完成。开发人员是无法自己完成这样的操作的。

在同一线程中,是无法有两个处于active状态的事务的。因此连续的调用UserTransaction接口的begin方法是会导致异常发生的。然而一个线程是可以关联多个事务的,否则事务的挂起和唤起从何谈起。

显而易见,begin()方法是事务的唯一出发点。而让事务结束,可以选择的方法就稍微多那么几个:

  1. rollback() - 让当前事务立即回滚。
  2. setRollbackOnly() - 标记当前事务需要回滚。这个方法的作用和上面在介绍CMT时,提及的EJBContext对象中的 setRollbackOnly()方法起到的作用是一模一样的。
  3. setTransactionTimeout(int seconds) - 规定一个执行时限,如果事务在规定的时间内还没有完成,就会导致事务的回滚。这个时限需要在事务开始之前就指定好,在事务执行途中是无法变更的。

最后,来看看getStatus()方法。这个方法用来得到当前执行线程的事务状态,它的返回值是一个名为javax.transaction.枚举类型。在这个枚举类型中,定义了多达10种状态。举几个例子:

  • STATUS_ACTIVE : 当前线程正处于一个进行状态下事务中。
  • STATUS_NO_TRANSACTION : 当前线程并不处于事务中。
  • STATUS_MARKED_ROLLBACK : 当前线程处于事务中,但是该事务已经被标记为需要回滚了。

在使用UserTransaction完成事务的操作时,一定需要注意合理处理各种编译时异常。尽管这些异常的种类确实是有些多,这也是为什么提倡使用CMT而非BMT的原因之一吧。


在下篇文章中,会介绍Spring Framework是如何处理事务划分的。

本系列:



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部